namespace pMedian {
    export class VNS<T> {
        private _demands: Map<string, number>
        public get demands(): Array<[T, number]> {
            let result = new Array<[T, number]>(this._demands.size);
            let idx = 0;
            this._demands.forEach((degeneracy, demandKey) => {
                result[idx] = [this.unhash(demandKey), degeneracy];
                idx++
            });
            return result;
        }

        private _facilities: Array<string>;
        public get facilities(): Array<T> {
            let result = new Array<T>();
            for (const [idx, facility] of this._facilities.entries()) {
                result[idx] = this.unhash(facility);
            }
            return result;
        }

        private _metricLookup: Map<string, number>;
        private metric(facilityKey: string, demandKey: string): number {
            return this._metricLookup.get(facilityKey + demandKey)
        }

        private _demandNeighbors: Map<string, Map<string, number>>;
        private _facilityNeighbors: Map<string, Map<string, number>>;

        /**
         * Creates an instance of GA_pMeanSolver.
         * @param {Array<T>} demands
         * @param {Array<T>} facilities
         * @param {Number} _n - number of facilities to be placed
         * @param {(arg0: T, arg1: T) => Number} metric: a function to measure the distance from a demand to some facility
         * @memberof GA
         */
        constructor(
            demands: Array<T>,
            facilities: Array<T>,
            private _n: number,
            metric: (facility: T, demand: T) => number,
            private hash: (t: T) => string,
            private unhash: (hash: string) => T
        ) {
            this._demands = new Map<string, number>();
            let demandsMap = new Map<string, number>();
            demands.forEach(demand => {
                let demandKey = this.hash(demand);
                if (this._demands.has(demandKey)) {
                    this._demands.set(demandKey, this._demands.get(demandKey) + 1);
                } 
                else {
                    this._demands.set(demandKey, 1);
                }
            });

            this._facilities = new Array<string>(facilities.length);
            this._metricLookup = new Map<string, number>();
            for (const [idx, facility] of facilities.entries()) {
                let facilityKey = this.hash(facility);
                this._facilities[idx] = facilityKey;
                for (const demandKey of this._demands.keys()) {
                    this._metricLookup.set(facilityKey + demandKey, this.metric(facilityKey, demandKey));
                }
            }
        }

        public initialSolution(): void {
            // first facility to place: 1-median
            let candidates = this._facilities.slice(0);
            let minTotalCost = Number.MAX_VALUE;
            let firstFacility = candidates[0];
            let firstFacilityIdx = 0;
            let costs: Array<number>
            this._demandNeighbors = new Map<string, Map<string, number>>();
            for (const [i, facility] of candidates.entries()) {
                let totalCost = 0;
                let thisCosts = new Array<number>(this._demands.size);
                let idx = 0;
                this._demands.forEach((degeneracy, demand) => {
                    let cost = degeneracy * this.metric(facility, demand);
                    totalCost += cost;
                    thisCosts[idx] = cost;
                    idx++;
                });
                if (totalCost < minTotalCost) {
                    minTotalCost = totalCost;
                    firstFacility = facility;
                    firstFacilityIdx = i;
                    costs = thisCosts;
                }
            }
            candidates.splice(firstFacilityIdx, 1);
            let neighbor = new Map<string, number>();
            let idx = 0;
            for (const demand of this._demands.keys()) {
                neighbor.set(demand, costs[idx]);
                idx++;
            }
            this._demandNeighbors.set(firstFacility, neighbor);

            // greadily find the n - 1 left facilities to place
            for (let i = 1; i < this._n; i++) {
                let maxDecreasedCost = Number.MAX_VALUE;
                let facilityToPlace = candidates[0];
                let facilityToPlaceIdx = 0;
                let verticeToModify: Array<[string, Array<[string, number]>]>;
                for (const [i, facility] of candidates.entries()) {
                    let decreasedCost = 0;
                    let verticeToModifyCandidate = new Array<[string, Array<[string, number]>]>();
                    for (const [openedFacility, vertice] of this._demandNeighbors.entries()) {
                        let demandsToReplace = new Array<[string, number]>();
                        for (const [demand, cost] of vertice.entries()) {
                            let newCost = this._demands.get(demand) * this.metric(facility, demand);
                            if (newCost < cost) {
                                decreasedCost += cost - newCost;
                                demandsToReplace.push([demand, newCost]);
                            }
                        }
                        verticeToModifyCandidate.push([openedFacility, demandsToReplace]);
                    }
                    if (decreasedCost > maxDecreasedCost) {
                        facilityToPlace = facility;
                        facilityToPlaceIdx = i;
                        verticeToModify = verticeToModifyCandidate;
                    }
                }
                candidates.splice(facilityToPlaceIdx, 1);
                let newNeighbor = new Map<string, number>();
                verticeToModify.forEach(tuple0 => {
                    let openedFacilityKey = tuple0[0];
                    let thisNeighbor = this._demandNeighbors.get(openedFacilityKey);
                    tuple0[1].forEach(tuple1 => {
                        let demandKey = tuple1[0];
                        let newCost = tuple1[1];
                        thisNeighbor.delete(demandKey);
                        newNeighbor.set(demandKey, newCost);
                    });
                    this._demandNeighbors.set(openedFacilityKey, thisNeighbor);
                });
                this._demandNeighbors.set(facilityToPlace, newNeighbor);
            }
            
            this._facilityNeighbors = new Map<string, Map<string, number>>();
            for (const placedFacility of this._demandNeighbors.keys()) {
                this._facilityNeighbors.set(placedFacility, new Map<string, number>());
            }
            candidates.forEach(facility => {
                let minDistance = Number.MAX_VALUE;
                let closestFacility = facility;
                for (const placedFacility of this._demandNeighbors.keys()) {
                    let distance = this.metric(placedFacility, facility);
                    if (distance < minDistance) {
                        minDistance = distance;
                        closestFacility = placedFacility;
                    }
                }
                this._facilityNeighbors.get(closestFacility).set(facility, minDistance);
            });
        }

        public shake(k: number): string {
            let kthNeighbor = this._facilityNeighbors.get(Array.from(this._facilityNeighbors.keys())[k]);
            let closedFacilities = Array.from(kthNeighbor.keys());
            return closedFacilities[Math.floor(Math.random() * closedFacilities.length)]
        }

        private substitute(
            demandNeighbors: Map<string, Map<string, number>>, 
            facilityNeighbors: Map<string, Map<string, number>>, 
            openedFacility: string, 
            closedFacilities: string
            ): void {

            }

        public vns(): void {
            let k = 0;
            let placedFacilityKeys = new Array<string>(this._n)
            while (k < this._n) {

            }
        }
    }
    
    /**
     * P-median solver, an janky implementation of 
     * https://link.springer.com/content/pdf/10.1023/A:1026130003508.pdf
     *
     * @export
     * @class GA_pMedianSolver
     * @template T
     */
    export class GA<T> {
        private _demands: Map<string, number>
        public get demands(): Array<[T, number]> {
            let result = new Array<[T, number]>(this._demands.size);
            let idx = 0;
            for (const [demandKey, degeneracy] of this._demands.entries()) {
                result[idx] = [this.unhash(demandKey), degeneracy];
                idx++;
            }
            return result;
        }

        private _facilities: Array<string>;
        public get facilities(): Array<T> {
            let result = new Array<T>();
            for (const [idx, facility] of this._facilities.entries()) {
                result[idx] = this.unhash(facility);
            }
            return result;
        }

        private _density: number;
        public get density(): number {
            return this._density;
        }

        private _maxIter: number;
        public get maxIter(): number {
            return this._maxIter;
        }

        private _population: Array<Array<string>>;
        public get population(): Array<Array<string>> {
            return this._population;
        }

        private _populationSize: number;
        public get populationSize(): number {
            return this._populationSize;
        }

        private _metricLookup: Map<string, number>;
        private metric(facilityKey: string, demandKey: string): number {
            return this._metricLookup.get(facilityKey + demandKey)
        }
        
        /**
         * Creates an instance of GA_pMeanSolver.
         * @param {Array<T>} demands
         * @param {Array<T>} facilities
         * @param {Number} n - number of facilities to be placed
         * @param {(arg0: T, arg1: T) => Number} metic: a function to measure the distance from a demand to some facility
         * @memberof GA
         */
        constructor(
            demands: Array<T>,
            facilities: Array<T>,
            private n: number,
            metric: (facility: T, demand: T) => number,
            private hash: (t: T) => string,
            private unhash: (hash: string) => T,
            maxIter: number = 256
        ) {
            this._demands = new Map<string, number>();
            demands.forEach(demand => {
                let demandKey = this.hash(demand);
                if (this._demands.has(demandKey)) {
                    this._demands.set(demandKey, this._demands.get(demandKey) + 1);
                } 
                else {
                    this._demands.set(demandKey, 1);
                }
            });

            this._facilities = new Array<string>(facilities.length);
            this._metricLookup = new Map<string, number>();
            for (const [idx, facility] of facilities.entries()) {
                let facilityKey = this.hash(facility);
                this._facilities[idx] = facilityKey;
                for (const demandKey of this._demands.keys()) {
                    this._metricLookup.set(facilityKey + demandKey, metric(facility, this.unhash(demandKey)));
                }
            }

            this._density = Math.ceil(facilities.length / this.n);
            // this._maxIter = Math.ceil(this.demands.length * Math.sqrt(this.n));
            this._maxIter = maxIter;
            this._populationSize = this.calcPopulationSize();
        }

        private calcPopulationSize(): number {
            let s = this.combination(this.n, this._facilities.length);
            return Math.max(4, Math.ceil(this.n * Math.log(s) / 100 / this._density)) * this._density
        }

        public generatePopulations(size: number){
            let increment = Math.floor(size / this._density); // k
            this._population = new Array<Array<string>>(size);
            console.log(size, this._density, increment)

            let idx = 0;
            for (let i = 0; i < Math.floor(this._facilities.length / this.n); i++) {
                let pop = new Array<string>(this.n);
                for (let j = 0; j < this.n; j++) {
                    pop[j] = this._facilities[i * this.n + j]
                }
                this._population[idx] = pop;
                idx++;
            }

            // if n / p is not integer, fill the remaining slots with random facilities
            if (this._facilities.length % this.n > 0) {
                let pop = new Array<string>(this.n);
                let j = 0;
                for (let i = Math.floor(this._facilities.length / this.n) * this.n; i < this._facilities.length; i++) {
                    pop[j] = this._facilities[i];
                    j++;
                }
                pop = this.fillPopulation(pop, j);
                this._population[idx] = pop;
                idx++;
            }

            let pointer = 0;
            let cycleIdx = 0;
            while (cycleIdx < increment) {
                let pop = new Array<string>(this.n);
                for (let j = 0; j < this.n; j++) {
                    if (pointer >= this._facilities.length) {
                        cycleIdx++;
                        pointer = cycleIdx
                    }
                    pop[j] = this._facilities[pointer];
                    pointer += increment;
                }
                this._population[idx] = pop;
                idx++
            }

            while (idx < size) {
                let pop = new Array<string>(this.n);
                this.fillPopulation(pop, 0)
                this._population[idx] = pop;
                idx++;
            }
        }

        public isIdenticalPop(a: Array<string>, b: Array<string>): boolean {
            a.sort();
            b.sort();
            let result = true;
            for (const [idx, entry] of a.entries()) {
                if (!(b[idx] === entry)) {result = false; break}
            }
            return result
        }

        private fillPopulation(population: Array<string>, start: number): Array<string> {
            let remainingFacilities = new Array<string>();
            let excludes = new Set<string>();
            population.slice(0, start).forEach(pop=> {
                excludes.add(pop);
            });
            this._facilities.forEach(facility => {
                if (!excludes.has(facility)) {
                    remainingFacilities.push(facility)
                }
            });

            misc.shuffle(remainingFacilities)
            for (let i = start; i < population.length; i++) {
                population[i] = remainingFacilities[i];
            }
            return population
        }

        public selectParents(): [Array<string>, Array<string>] {
            let idxes = new Array<number>(this._populationSize);
            for (let idx = 0; idx < this._populationSize; idx++) {
                idxes[idx] = idx;
            }
            let parentsIdx = misc.shuffle(idxes).slice(0, 2);
            return [this._population[parentsIdx[0]], this._population[parentsIdx[1]]]
        }

        public generationOp(
            parents: [Array<string>, Array<string>]
        ): [Array<string>, number] {
            // let genes0 = new misc.ObjectSet(parents[0], this.isEqual, this.hash);
            // let genes1 = new misc.ObjectSet(parents[1], this.isEqual, this.hash);
            let genes0 = new Set<string>();
            parents[0].forEach(gene => {
                genes0.add(gene);
            });
            let genes1 = new Set<string>();
            parents[1].forEach(gene => {
                genes0.add(gene);
            });

            // Step 1. Take the union of the input members’ genes to obtain a draft member
            let draftMemberSet = misc.union(genes0, genes1);

            /* Step 2. Let the total number of genes in this draft member be m. Call the genes that are 
            * present in both parents fixed genes and the rest free genes.
            */
            let fixedGenes = misc.intersection(genes0, genes1);
            let freeGeneSet = misc.difference(draftMemberSet, fixedGenes);
            let freeGenes = Array.from(freeGeneSet);

            // Step 3. Compute the fitness value of this draft member.
            let distances = new Map<string, Map<string, number>>();
            freeGenes.forEach(gene => {
                distances.set(gene, new Map());
            });

            for (const demand of this._demands.keys()) {
                let minDistance = Number.MAX_VALUE;
                let closestFacility = freeGenes[0];
                freeGenes.forEach(facility => {
                    let distance = this.metric(facility, demand);
                    if (distance < minDistance) {
                        minDistance = distance;
                        closestFacility = facility;
                    }
                });
                let distance = distances.get(closestFacility);
                distance.set(demand, minDistance);
                distances.set(closestFacility, distance);
            }
            
            
            /* Step 4. Find the free gene that produces the minimum increase in the current fitness
            * value when deleted from the draft member, and delete it. Repeat this step until
            * freeGenes.length = n. Let this final solution be a candidate member.
            */
            while (freeGenes.length > this.n) {
                let minIncreasedFitness = Number.MAX_VALUE;
                let idxOfGeneToRemove = 0;
                let distancesUpdate = new Map<string, Map<string, number>>();

                // try to delete one gene(facility), calculate how much the total fitness increases
                for (const [idx, gene] of freeGenes.entries()) {
                    let demandsToReallocate = distances.get(gene);
                    let increasedFitness = 0;
                    let update = new Map();
                    
                    demandsToReallocate.forEach((_, demand) => {
                        let minDistance = Number.MAX_VALUE;
                        let closestFacility = gene;
                        for (const [idx1, gene1] of freeGenes.entries()) {
                            // skip the deleted gene
                            if (idx1 == idx) {continue};
                            let distance = this.metric(gene1, demand);
                            if (distance < minDistance) {
                                minDistance = distance;
                                closestFacility = gene1;
                            };
                        };

                        let degeneracy = this._demands.get(demand);
                        increasedFitness += degeneracy * (minDistance - distances.get(gene).get(demand));
                        if (!update.has(closestFacility)) {update.set(closestFacility, new Map());}
                        let distance = update.get(closestFacility);
                        distance.set(demand, minDistance);
                        update.set(closestFacility, distance);
                    });

                    if (increasedFitness < minIncreasedFitness) {
                        minIncreasedFitness = increasedFitness;
                        idxOfGeneToRemove = idx;
                        distancesUpdate = update;
                    };
                }

                // the gene to remove is found, remove it
                distances.delete(freeGenes[idxOfGeneToRemove]);
                distancesUpdate.forEach((map, gene) => {
                    let facility = distances.get(gene);
                    map.forEach((distance, demand) => {
                        facility.set(demand, distance);
                    });
                    distances.set(gene, facility);
                });
                freeGenes.splice(idxOfGeneToRemove, 1);
            }

            let fitness = 0;
            distances.forEach((facility, _) => {
                facility.forEach((distance, demandKey) => {
                    let degeneracy = this._demands.get(demandKey);
                    fitness += distance * degeneracy;
                });
            });

            return [freeGenes, fitness]
        }

        public fitness(genes: Array<string>): number {
            let fitness = 0;
            this._demands.forEach((degeneracy, demand) => {
                let minDistance = Number.MAX_VALUE;
                genes.forEach(facility => {
                    let distance = this.metric(facility, demand);
                    if (distance < minDistance) {minDistance = distance;}
                });
                fitness += degeneracy * minDistance;
            });
            return fitness
        }

    public replacementOp(
        candidate: Array<string>, 
        candidateFitness: number, 
        fitnessEnum: Array<[number, number]>,
        comparator: (a: [number, number], b: [number, number]) => number
        ) {
        let idx = 0;
        let maxFitness = 0;
        [idx, maxFitness] = fitnessEnum[0];

        /**
         * Step 1. If fitness value of the input candidate member is higher than the maximum fitness
         * value in the population, then discard this candidate member and terminate this
         * operator
         */
        if (candidateFitness >= maxFitness) {return}

        /**
         * Step 2. If the candidate member is identical to an existing member of the current popu-
         * lation, then discard this candidate member and terminate this operator.
         */
        for (const [_, pop] of this._population.entries()) {
            if (this.isIdenticalPop(candidate, pop)) {return}
        }

        // Step 3. Replace the worst member and update population
        // fitnessEnum.shift();
        fitnessEnum.splice(0, 1, [idx, candidateFitness])
        fitnessEnum.sort(comparator)
        this._population.splice(idx, 1, candidate);
        // misc.binaryInsert([idx, candidateFitness], fitnessEnum, comparator);
    }

        /**
         * Combination (n, k)
         * @private
         * @param {number} n
         * @param {number} k
         * @returns {number}
         * @memberof GA_pMeanSolver
         */
        private combination(n: number, k: number): number{
            if (n > Math.floor(k / 2)) {
                n = k - n;
            }

            let result: number = 1;
            for (let i = 0; i < n; i++) {
                result *= (k - i) / (n - i)
            }

            return result
        }

        public pMedian(log: (entry: string) => void): Array<T> {
            this.generatePopulations(this._populationSize);

            let comparator = function (a: [number, number], b: [number, number]): number {
                if (a[1] > b[1]) {return -1}
                if (a[1] == b[1]) {return 0}
                return 1
            };
            
            let fitnessEnum = new Array<[number, number]>();
            for (const [idx, pop] of this._population.entries()) {
                let fitness = this.fitness(pop);
                fitnessEnum.push([idx, fitness])
            };
            fitnessEnum.sort(comparator);

            let iter = 0;
            let best = this._population[fitnessEnum[fitnessEnum.length - 1][0]];
            while (iter < this._maxIter) {
                // Randomly select two members from the current population.
                let parents = this.selectParents();

                // Run the Generation Operator
                let candidate: Array<string>;
                let candidateFitness: number;
                [candidate, candidateFitness] = this.generationOp(parents);

                // Run the Replacement Operator
                this.replacementOp(candidate, candidateFitness, fitnessEnum, comparator);
                // console.log(fitnessEnum)

                // If the best solution found so far has not changed, then increment MaxIter
                let bestThisIter = this._population[fitnessEnum[fitnessEnum.length - 1][0]];
                // console.log('iter:', iter, '/', this.maxIter, fitnessEnum[fitnessEnum.length - 1][1], fitnessEnum[0][1], candidateFitness)
                log('iter: ' + ' ' + iter + ' ' + '/' + ' ' + 
                this.maxIter + ' ' + fitnessEnum[fitnessEnum.length - 1][1]
                 + ' ' + fitnessEnum[0][1] + ' ' + candidateFitness);
                if (this.isIdenticalPop(bestThisIter, best)) {
                    iter++;
                }
                else {
                    best = bestThisIter;
                }
            }
            return best.map(this.unhash)
        }
    }
}