/// <summary>
        ///
        /// </summary>
        /// <param name="data"></param>
        /// <param name="costCoefficient">way will be multiplied with this</param>
        /// <param name="startCost">Additional cost from home to any visit</param>
        public CostEvaluator(RoutingData data, int costCoefficient, int startCost)
        {
            timeEvaluator = new TimeEvaluator(data);

            this.data            = data;
            this.costCoefficient = costCoefficient;
            this.startCost       = startCost;
        }
        /// <summary>
        /// requires data.SantaIds
        /// requires data.Visits
        /// requires data.HomeIndex
        /// requires data.Unavailable
        /// requires data.Start
        /// requires data.End
        /// </summary>
        public static OptimizationResult Solve(RoutingData data, long timeLimitMilliseconds, ITimeWindowStrategy strategy)
        {
            if (false ||
                data.SantaIds == null ||
                data.Visits == null ||
                data.Unavailable == null ||
                data.SantaStartIndex == null ||
                data.SantaEndIndex == null
                )
            {
                throw new ArgumentNullException();
            }

            var model = new RoutingModel(data.Visits.Length, data.SantaIds.Length, data.SantaStartIndex, data.SantaEndIndex);

            // setting up dimensions
            var maxTime      = GetMaxTime(data);
            var timeCallback = new TimeEvaluator(data);

            model.AddDimension(timeCallback, 0, maxTime, false, DimensionTime);
            var lengthCallback = new TimeEvaluator(data);

            model.AddDimension(lengthCallback, 0, maxTime, true, DimensionLength);

            // set additional cost of longest day
            {
                var dim = model.GetDimensionOrDie(DimensionLength);
                dim.SetGlobalSpanCostCoefficient(data.Cost.CostLongestDayPerHour);
            }

            // dimensions for breaks
            var breakCallbacks  = new List <BreakEvaluator>();
            var breakDimensions = new List <string>();

            for (int santa = 0; santa < data.NumberOfSantas; santa++)
            {
                var maxBreaks = GetNumberOfBreaks(data, santa);
                if (maxBreaks == 0)
                {
                    // no breaks
                    continue;
                }

                var evaluator = new BreakEvaluator(data, santa);
                var dimension = GetSantaBreakDimension(santa);
                model.AddDimension(evaluator, 0, maxBreaks, true, dimension);
                breakCallbacks.Add(evaluator);
                breakDimensions.Add(dimension);
            }

            // setting up santas (=vehicles)
            var costCallbacks = new NodeEvaluator2[data.NumberOfSantas];

            for (int santa = 0; santa < data.NumberOfSantas; santa++)
            {
                // must be a new instance per santa
                NodeEvaluator2 costCallback = data.Input.IsAdditionalSanta(data.SantaIds[santa])
                    ? new CostEvaluator(data, data.Cost.CostWorkPerHour + data.Cost.CostAdditionalSantaPerHour, data.Cost.CostAdditionalSanta)
                    : new CostEvaluator(data, data.Cost.CostWorkPerHour, 0);
                costCallbacks[santa] = costCallback;
                model.SetVehicleCost(santa, costCallback);

                // limit time per santa
                var day   = data.GetDayFromSanta(santa);
                var start = GetDayStart(data, day);
                var end   = GetDayEnd(data, day);
                model.CumulVar(model.End(santa), DimensionTime).SetRange(start, end);
                model.CumulVar(model.Start(santa), DimensionTime).SetRange(start, end);

                // avoid visiting breaks of other santas
                var breakDimension = GetSantaBreakDimension(santa);
                foreach (var dimension in breakDimensions.Except(new[] { breakDimension }))
                {
                    model.CumulVar(model.End(santa), dimension).SetMax(0);
                }
            }

            // setting up visits (=orders)
            for (int visit = 0; visit < data.NumberOfVisits; ++visit)
            {
                var cumulTimeVar = model.CumulVar(visit, DimensionTime);
                cumulTimeVar.SetRange(data.OverallStart, data.OverallEnd);
                model.AddDisjunction(new[] { visit }, data.Cost.CostNotVisitedVisit);

                // add desired / unavailable according to strategy
                var timeDimension = model.GetDimensionOrDie(DimensionTime);
                strategy.AddConstraints(data, model, cumulTimeVar, timeDimension, visit);
            }

            // Solving
            var searchParameters = RoutingModel.DefaultSearchParameters();

            searchParameters.FirstSolutionStrategy    = FirstSolutionStrategy.Types.Value.Automatic; // maybe try AllUnperformed or PathCheapestArc
            searchParameters.LocalSearchMetaheuristic = LocalSearchMetaheuristic.Types.Value.GuidedLocalSearch;
            searchParameters.TimeLimitMs = timeLimitMilliseconds;

            var solution = model.SolveWithParameters(searchParameters);

            // protect callbacks from the GC
            GC.KeepAlive(timeCallback);
            GC.KeepAlive(lengthCallback);
            foreach (var costCallback in costCallbacks)
            {
                GC.KeepAlive(costCallback);
            }
            foreach (var breakCallback in breakCallbacks)
            {
                GC.KeepAlive(breakCallback);
            }

            Debug.WriteLine($"obj={solution?.ObjectiveValue()}");

            return(CreateResult(data, model, solution));
        }