import * as d3 from './d3-custom';

// views and interactive elements
import { createScrollView } from './scrollView';
import { createEnvironment } from './environment';
import { createInfoView } from './infoView';
import { createSelectionList } from './selectionList';
import { createProgressBar } from './progressBar';
import { createReferenceClass } from './referenceClass';
import { createZoomButtons } from './zoomButtons';

// configuration and utilities
import { interpolate } from './utilities';
import { PDFExport } from './pdfExport';
import config from './config.json';

export class Controller {

    constructor() {

        this.cMaxVal = undefined;           // maximum value of currently selected data set
        this.cMinVal = undefined;           // minimum value of currently selected data set
        this.cMaxScale = undefined;         // zoomed in
        this.cMinScale = undefined;         // zoomed out
        this.cScale = undefined;            // interpolated scale corresponding to current scroll height

        this.nScrollRatio = 0;              // normalised scroll ratio [0..1]
        this.scrollY = 0;                   // absolute scroll position
        this.eTotalHeight = 0;              // effective height for scrolling (scroll container height - viewport height)

        this.newData = false;               // true if data has changed recently
        this.currentEntries = [];           // entries filtered by gender and age drop down menus
        this.visibleEntries = [];           // entries inside the view frustum (potentially visible)
        this.hiddenEntries = [];            // entries outside of the view frustum (technically and conceptually invisible)
        this.selectedEntries = [];
        this.initialised = false;

        this.commonOptions;                      // Common options for components
    }

    init() {

        for (let key in config) {
            this[key] = config[key];
        }

        this.commonOptions = {
            colours: this.colours,
            controller: this,
            font: this.font,
            labels: this.labels.info,
            lang: this.lang,
            spaceFormat: this.spaceFormat,
            durations: this.durations
        };

        this.margin = {
            top: this.borderWidth,
            right: this.borderWidth,
            bottom: this.borderWidth,
            left: this.borderWidth,
        };

        this.labelOffset = this.font.ratio.medium * this.font.baseSize;      // 1em corresponding to large font size
        this.entryBarHeight = 3 * this.font.ratio.small * this.font.baseSize;   // maximum height of bar (when displayed at bottom of triangle)

        let option = window.location.search.substr(1,4);
        if(option === 'lang') {
            this.lang = window.location.search.substr(6, window.location.search.length - 1);
        }

        this.labels.info.path = location.pathname.substr(0, location.pathname.lastIndexOf('/') + 1);

        document.title = this.labels.info.title[this.lang];

        this.loadData(this, this.uri[this.lang]);

        this.pdfx = new PDFExport(this, this.labels.info, this.lang);
        this.iform = d3.select('#iform');

        d3.select('#gender-label').text(this.labels.controls.gender.all[this.lang]);
        d3.select('#gender-option').text(this.labels.controls.gender.all[this.lang]);
        d3.select('#gender-female').text(this.labels.controls.gender.female[this.lang]);
        d3.select('#gender-male').text(this.labels.controls.gender.male[this.lang]);
        d3.select('#age-label').text(this.labels.controls.age[this.lang]);
        d3.select('#age-option').text(this.labels.controls.age[this.lang]);

        d3.select('body').append('div')
            .attr('id', 'fake');

        this.svg = d3.select('.chart svg');
        this.svg.attr('class', 'interactive');

        this.initLayout();

        this.createSVGElements();
    }

    createSVGElements() {

        this.createBackground();

        this.sView = createScrollView({
            common: this.commonOptions,
            boundingBox: this.streetBBox,
            barHeight: this.entryBarHeight,
            offset: { x: this.labelOffset, y: this.offset.y }
        });
        this.svg.call(this.sView);

        this.createForeground();
    }

    initLayout() {

        let visibleHeight = window.innerHeight;

        this.svgWidth = this.width - this.margin.left - this.margin.right;
        this.svgHeight = this.height - this.controlBarHeight - this.margin.top - this.margin.bottom;

        d3.select('#fake').style('height', this.scrollHeight + 'px');  // set height of scroll container
        this.eTotalHeight = this.scrollHeight - visibleHeight;  // subtract visible height

        // 'Responsive' layout: scale to full width, preserve aspect ratio
        this.svg.attr('preserveAspectRatio', 'xMidYMid meet');

        this.svg.attr('viewBox', '0 0 ' + this.svgWidth + ' ' + this.svgHeight)
            .attr('preserveAspectRatio', 'xMidYMid meet');

        this.farClippingY = 0.3 * this.svgHeight;
        this.progressBarHeight = this.svgHeight - 4 * this.font.ratio.large * this.font.baseSize;
        this.offset = {
            x: 0,
            y: 0.3 * this.font.ratio.large * this.font.baseSize
        };

        // bottom left, top right
        this.streetBBox = {
            min: { x: this.offset.x, y: this.farClippingY + this.offset.y },
            max: { x: this.svgWidth * 0.66, y: this.svgHeight }
        };

        this.streetBBox.width = this.streetBBox.max.x - this.streetBBox.min.x;
        this.streetBBox.height = this.streetBBox.max.y - this.streetBBox.min.y;
        this.vanishingPoint = { x: 0.66 * this.streetBBox.width, y: this.streetBBox.min.y };
    }

    loadData(self, uri) {
        d3.json(uri, function (_data) {

            self.data = _data;

            self.data.reasons.forEach(function (reason) {
                let color = d3.color('#000000');
                switch (reason.cat) {
                    case self.labels.categories[self.lang][0]: color = d3.color(self.colours.categories[0]); break;
                    case self.labels.categories[self.lang][1]: color = d3.color(self.colours.categories[1]); break;
                    case self.labels.categories[self.lang][2]: color = d3.color(self.colours.categories[2]); break;
                    case self.labels.categories[self.lang][3]: color = d3.color(self.colours.categories[3]); break;
                    case self.labels.categories[self.lang][4]: color = d3.color(self.colours.categories[4]); break;
                    default: color = d3.color('#888'); console.error('unknown reason "'+reason.cat+'"');
                }
                reason.colorFill = color;
                reason.colorText = color;
            });

            self.data.filters.forEach(function (filter) {
                filter.entries = filter.entries.map(function (value, index) {
                    return {
                        value: value,
                        reason: self.data.reasons[index]
                    };
                });
                filter.entries.sort(function (a,b) {
                    // return b.value - a.value;
                    return a.value - b.value;
                });
            });

            self.updateData();
        });
    }

    updateData() { // called every time when data (selection) changes
        if (!this.data) {
            console.error('no data!');
            return;
        } // selected values for age, gender
        let alter = d3.select('#alter').node().value;
        let geschlecht = d3.select('#geschlecht').node().value;

        // Hole die entsprechenden Daten aus dem JSON
        let entry = this.data.filters.filter(function (entry) {
            if (entry.alter !== alter) return false;
            if (entry.geschlecht !== geschlecht) return false;
            return true;
        });
        if (entry.length !== 1) return console.error(entry);
        entry = entry[0];

        let previousValues = [];

        if(this.initialised === true) {
            previousValues = this.currentEntries;
        }
        this.currentEntries = entry.entries.slice(0);

        this.currentEntries.sort(function (a,b) {
            // return b.value - a.value;  // large -> small
            return a.value - b.value;  // small -> large
        });


        this.cMaxVal = d3.max(this.currentEntries, function(d) { return d.value; });
        // Element with highest value placed at bottom of window ("zoomed out")  // scrollTop(0)
        this.cMinScale = (this.streetBBox.height - this.entryBarHeight) / this.cMaxVal;
        this.cMinVal = d3.min(this.currentEntries, function(d) { return d.value; });
        // Element with lowest value placed at bottom of window ("zoomed in")  // scrollTop(this.eTotalHeight)
        this.cMaxScale = (this.streetBBox.height - this.entryBarHeight) / this.cMinVal;

        this.currentEntries.forEach(function(entry, i){
            entry.id = i;
            entry.previousValue = previousValues[i] ? previousValues[i].value : undefined;
            //  scale to display as foremost entry
            entry.scaleMax = (this.streetBBox.height - this.entryBarHeight) / entry.value;
            entry.selected = entry.selected ? true : false;
        }, this);

        // search / datalist
        let rlist = d3.select('#result-list');
        rlist.selectAll('*').remove();
        let self = this;

        this.currentEntries.forEach(function(entry) {
            rlist.append('option')
                .attr('value', entry.reason.title)
                .attr('id', entry.id)
                .html(entry.reason.title);
        });

        d3.select('#search')
            .on('input', function() {
                self.currentEntries.forEach(function(e) {
                    if (e.reason.title === this.value) {
                        self.scrollToEntry(e);
                    }
                }, this);
            });

        this.updateLayout();
    }

    // called every time the presentation changes due to user interaction
    updateLayout() {

        // scale corresponding to current scroll position
        this.cScale = interpolate(this.cMaxScale, this.cMinScale, this.nScrollRatio, this.interpolationMethod);
        this.visibleEntries = [];
        this.hiddenEntries = [];
        this.currentEntries.forEach(function(entry){

            // y position of entry for current scale (relative to entries group)
            entry.y = entry.value * this.cScale;
            // linear interpolation for e.g. scaling of bars
            entry.ratio = (entry.y / (this.streetBBox.height - this.entryBarHeight));
            // linear interpolation of x coordinate of bar
            entry.x =  (1 - entry.ratio) * this.vanishingPoint.x + entry.ratio * this.streetBBox.min.x;
            if (this.cScale <= entry.scaleMax) {
                entry.visibility = 'visible';
                this.visibleEntries.push(entry);
            } else {
                this.hiddenEntries.push(entry);
                entry.visibility = 'hidden';
            }
        }, this);

        // let ttime = ((this.ptime - this.stime) / 1000);
        this.visibleEntries.sort(function(a, b) {  // update the drawing order of the svg elements
            // return a.y - b.y;
            return b.y - a.y;
        });

        this.pBar.max(this.cMaxVal);
        // this.environment.minVal(this.cMinVal);
        this.sView.entries(this.currentEntries);

        if(this.initialised === false) {
            this.createRandomSelection();
            this.updateSelectedEntries();
            this.initialised = true;
        }

        this.listSelectedEntries();

        this.draw();
    }

    // draw all dynamic elements
    // environment (street markings)
    // progress bar (no transition on data change so far...)
    // scroll view (main view with all entries)
    draw() {
        this.environment.draw(this.cScale, this.newData);
        this.pBar.draw(interpolate(this.cMinScale / this.cMaxScale, 1.0, this.nScrollRatio, this.interpolationMethod));
        this.sView.update(this.newData);
    }

    createBackground() {  // draw only once and then fagetaboutit

        this.background = this.svg.append('g')                    // background group
            .attr('class', 'background');

        this.environment = createEnvironment();
        this.environment
            .markingUnit(this.entryBarHeight)
            .streetBBox(this.streetBBox)
            .streetLength(this.scrollHeight)
            .vanishingPoint(this.vanishingPoint)
            .markingColour(this.colours.translucent)
            .skyWidth(this.svgWidth)
            .skyHeight(this.svgHeight);

        this.background.call(this.environment);

    }

    createForeground() {

        this.foreground = this.svg.append('g')
            .attr('class', 'foreground');

        // fog of uncertainty
        this.foreground.append('rect')
            .attr('class', 'fog')
            .attr('x', 0)
            .attr('y', this.vanishingPoint.y)
            .attr('width', this.svgWidth)
            .attr('height', this.svgHeight)
            .attr('pointer-events', 'none')
            .attr('fill', 'url(#fog-gradient)');

        this.iview = createInfoView({
            common: this.commonOptions,
            width: this.vanishingPoint.x,
            height: this.vanishingPoint.y
        });
        this.foreground.call(this.iview);

        this.sList = createSelectionList({
            common: this.commonOptions,
            maxEntries: this.components.selectionList.maxEntries,
            x: this.streetBBox.width - this.labelOffset - 2 * this.font.ratio.medium * this.font.baseSize,
            y: 0.75 * this.entryBarHeight,
            width: this.components.selectionList.width,
            height: this.components.selectionList.height
        });
        this.foreground.call(this.sList);

        this.pBar = createProgressBar({
            common: this.commonOptions,
            max: this.cMaxVal,
            x: this.svgWidth - this.entryBarHeight * 1.25,
            y: 0.82 * this.entryBarHeight,
            width: this.entryBarHeight,
            height: this.progressBarHeight
        });
        this.foreground.call(this.pBar);

        this.refClass = createReferenceClass({
            common: this.commonOptions,
            x: this.svgWidth - 6.5 * this.font.ratio.medium * this.font.baseSize,
            y: this.streetBBox.max.y - this.entryBarHeight / 2 + this.labelOffset / 2
        });
        this.foreground.call(this.refClass);

        this.zoomBtns = createZoomButtons({
            common: this.commonOptions,
            x: this.svgWidth - this.font.ratio.large * this.font.baseSize * 1.3,
            y: this.streetBBox.max.y - this.font.ratio.large * this.font.baseSize * 2
        });
        this.foreground.call(this.zoomBtns);
    }

    scrollPosition() { // cross-browser, check on more browsers, OS
        return (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
    }

    scrollTopTween(st) { // natural scrolling. Test this!!! (IE)
        let self = this;
        return function() {
            let i = d3.interpolateNumber(self.scrollPosition(), st);
            let scrollElement = document.scrollingElement || document.documentElement;
            return function(t) {
                scrollElement.scrollTop = i(t);
            };
        };
    }

    scrolled() {
        this.scrollY = this.scrollPosition();
        this.nScrollRatio = this.scrollY / this.eTotalHeight;
        this.updateLayout();
    }


    // scroll to entry
    nextEntry() {
        // adapt distance measure to scale to compensate for numerical errors in small value ranges
        const dist = this.visibleEntries[0].scaleMax / this.cScale - 1;
        return dist <= 0.001 ? this.visibleEntries[1] : this.visibleEntries[0];  // account for numerical errors
    }

    previousEntry() {
        return this.hiddenEntries.length > 0 ? this.hiddenEntries[0] : undefined;
    }

    /**
     * Calculates the corresponding scroll position for the provided target
     * and scrolls to that position, so that target is displayed at the bottom
     * of the *street* in maximum scale
     * @param target: d3 style data entry
     */
    scrollToEntry(target) {

        if(target === undefined) {
            return;
        }
        let quotient = 0;
        // solve interpolation equation for scroll position 't'
        if(this.interpolationMethod === 'linear') {
            quotient = (target.scaleMax - this.cMinScale) / (this.cMaxScale - this.cMinScale);
        } else if (this.interpolationMethod === 'logarithmic') {
            quotient = (Math.log(target.scaleMax) - Math.log(this.cMinScale)) / (Math.log(this.cMaxScale) - Math.log(this.cMinScale));
        }

        d3.select('body')
            .transition()
            .duration(this.durations.autoScroll)
            .tween('autoscroll', this.scrollTopTween(this.eTotalHeight * quotient));
    }

    zoomIn() {
        if(this.scrollPosition() === this.eTotalHeight) {
            return;
        }

        this.scrollToEntry(this.nextEntry());
    }

    zoomOut() {
        if(this.scrollPosition() === 0) {
            return;
        }

        this.scrollToEntry(this.previousEntry());
    }

    showInfo(target) {
        this.iview.showInfo(target);
    }

    deleteEntryFromSelection(target) {
        target.selected = false;
        this.updateSelectedEntries();
    }

    isEntrySelected(entry) {
        return this.getSelectedEntries().indexOf(entry) !== -1 ? true : false;
    }

    createRandomSelection() {
        let selectedEntries = [];
        let ra = 5;

        while (selectedEntries.length < ra) {
            let prob = 1.0 / this.currentEntries.length;
            let rnd = Math.random();

            let cumulatedPval = 0.0;

            let idx = 0;
            for (; idx < this.currentEntries.length; ++idx) {
                cumulatedPval += prob;

                if (rnd < cumulatedPval) {
                    break;
                }
            }
            let ce = this.currentEntries[idx];
            ce.selected = true;
            if (selectedEntries.indexOf(ce) === -1) {
                selectedEntries.push(this.currentEntries[idx]);
            }
        }
        return selectedEntries;
    }

    toggleSelection(target) {
        let selectedEntries = this.getSelectedEntries();

        if(selectedEntries.length === this.maxSelectionSize && target.selected === false) {  // limit selection list length
            return;
        }

        target.selected = !target.selected;
        this.updateSelectedEntries();
    }

    hideDownloadButton(visibility) {
        d3.select('#export')
            .classed('hidden', visibility);
    }

    getSelectedEntries() {
        let selectedEntries = this.currentEntries.filter(function(entry) {
            if (entry.selected === true) return true;
            return false;
        });
        return selectedEntries;
    }

    updateSelectedEntries() {
        this.selectedEntries = this.getSelectedEntries();
        this.pdfx.entriesListUpdated(this.selectedEntries);
        this.listSelectedEntries();
    }

    listSelectedEntries() {

        let selectedOption = d3.select('#category').node().selectedOptions[0].innerHTML;
        let categories = this.labels.categories[this.lang];
        let filter = categories.indexOf(selectedOption) !== -1 ? selectedOption : '';
        this.sList.update(this.selectedEntries, filter);
        this.draw();
    }

    setCategory(cat) {
        this.category = cat;
        this.sView.category(cat);
        this.listSelectedEntries();
        this.draw();
    }

    filterSelectionChanged() {
        this.newData = true;
        this.updateData();
    }

    dataProcessed() {
        this.newData = false;
    }
}
