import { GeoMap } from './map.js'
import { EventHandler } from './utils.js'
import { GeoGraphics } from './geographics.js'
import { MapList } from './maplist.js'
import MapViewport from './mapviewport.js'
import { ScatterContainer } from '../scatter.js'

/**
 * The GeoLayer is a special PIXILayer, that recognizes other GeoLayers and
 * GeoGraphics. The layer can be adapted to a map and notifies all Geo-Children
 * of the Adaption.
 *
 * @export
 * @class GeoLayer
 */
export class GeoLayer {
    constructor(displayObject, opts = {}) {
        if (displayObject == null || !(displayObject instanceof PIXI.DisplayObject)) {
            console.error(
                `You need to provide a displayObject to make a ${this.constructor.name} out of it.`,
                displayObject
            )
            return null
        } else {
            this.geographics = []
            // displayObject.map = this
            this.displayObject = displayObject

            this.pixiAddChild = displayObject.addChild.bind(displayObject)
            displayObject.addChild = (...elements) => {
                elements.forEach(element => {
                    if (element instanceof GeoGraphics) {
                        element.setLayer(this)
                        this.geographics.push(element)
                        this.pixiAddChild(element.graphics)
                    } else {
                        this.pixiAddChild(element)
                    }
                })
            }
        }

        this.name = opts['name'] != null ? opts.name : 'Unnamed Layer'
        this.parent = null
        this.parentMapLayerTransformedHandler = new EventHandler('onParentMapLayerTransformed')
        this.layers = []
        this._visibility = { min: 0, max: Number.MAX_VALUE }
    }

    parentMapLayerTransformed(mapLayer) {
        this.layers.forEach(layer => {
            if (!(layer instanceof MapList)) {
                layer.parentMapLayerTransformed()
            }
        })

        this.parentMapLayerTransformedHandler.call(null, mapLayer)

        this.rescaleChildren()
    }

    rescaleChildren() {
        let map = this.map

        if (this.rescale) {
            if (map != null) {
                let scale = map.image.scatter.scale

                this.displayObject.children.forEach(graphics => {
                    graphics.scale.set(1 / scale, 1 / scale)
                })
            }
        }

        let mapLayer = this.mapLayer
        if (this.visibility && mapLayer != null) {
            const zoom = mapLayer.mapview.zoom

            // TODO
            // Currently I dont know what elemnts was.
            // We just log an error and resolve this on a later point.
            if (zoom > this.visibility.min && zoom < this.visibility.max) {
                this.displayObject.children.forEach(it => (it.visible = true))
            } else {
                this.displayObject.children.forEach(it => (it.visible = false))
            }
        }
    }

    set visibility(value) {
        let { min = 0, max = Infinity } = value
        this._visibility = { min, max }
    }

    get visibility() {
        return this._visibility
    }

    /**
     * Alias for geoLayer.displayObject.addChild.
     *
     * @public
     * @param {GeoGraphics | PIXI.DisplayObject} element - Element to add to the displayObject.
     * @memberof GeoLayer
     */
    addChild(element) {
        this.displayObject.addChild(element)
    }

    /**
     * Adapts to a map. If the maplayer should adapt to the parent maplayer,
     * no parameter must be specified.
     */
    adapt(map = null) {
        if (!map) map = this.map

        if (map) {
            this.geographics.forEach(geographic => {
                geographic.adaptTo(map)
            })

            this.layers.forEach(layer => {
                if (layer.adapt) layer.adapt(map)
            })

            this.rescaleChildren()
        } else console.error('There was no map specified.', this)
    }

    removeFromParent() {
        if (this.parent) {
            this.parent.removeLayer(this)
        }
    }

    removeLayer(layer) {
        let idx = this.layers.indexOf(layer)
        if (idx != -1) {
            layer.parent = null
            this.layers.splice(idx, 1)

            if (layer.displayObject.parent) {
                layer.displayObject.parent.removeChild(layer.displayObject)
            }
        } else console.warn('Tried to remove layer that was not set.', this, layer)
    }

    remove(graphics) {
        if (graphics instanceof GeoGraphics) {
            let index = this.geographics.indexOf(geographics)
            if (index != -1) {
                this.displayObject.removeChild(geographics)
            } else {
                console.error('Could not remove geographics from geolayer.', this, geographics)
            }
        } else {
            this.displayObject.removeChild(graphics)
        }
    }

    set parent(parent) {
        this._parent = parent
    }

    get parent() {
        return this._parent
    }

    /**
     * Adds a GeoLayer as child to the GeoLayer.
     *
     * @public
     * @param {GeoLayer} layer - GeoLayer to add.
     * @memberof GeoLayer
     */
    addLayer(layer) {
        if (layer instanceof GeoLayer) {
            layer.removeFromParent()

            this.layers.push(layer)
            layer.parent = this
            layer.parentChanged()

            this.displayObject.addChild(layer.displayObject)
            if (this.map) layer.geographics.forEach(geographics => geographics.adaptTo(this.map))
        } else
            console.error('Could not place layer. Only MapLayer and GeoLayers can be child layers of GeoLayers.', layer)
    }

    parentChanged() {
        this.rescaleChildren()
    }

    //GeoLayers have to be children of a map layer,
    // therefore we can recursively get the map.
    get map() {
        return this.mapLayer ? this.mapLayer.map : null
    }

    get mapLayer() {
        return this._mapLayer ? this._mapLayer : this.parent.mapLayer
    }

    // clone(mapLayerClone) {
    //     const opts = {
    //         mapLayer: mapLayerClone,
    //         map: mapLayerClone.map
    //     }
    //     let geoLayerClone = new GeoLayer(opts)

    //     this.layers.forEach(layer => {
    //         let layerClone = layer.clone(opts)
    //         if (layerClone) {
    //             geoLayerClone.placeLayer(layerClone)
    //         }
    //     })

    //     this.geographics.forEach(geographics => {
    //         let clone = geographics.clone()
    //         if (clone) {
    //             geoLayerClone.place(clone)
    //         }
    //     })

    //     return geoLayerClone
    // }
}

/**
 * The map layer is responsible for showing certain maps, at a specific position It contains
 * a list of available maps and can switch between them seamlessly. GeoGraphics placed on the MapLayer itself
 * or child Geolayers will be adapted to maps and adjusted on map change automatically.
 *
 * The map layer is the 'king' of the geo layers. Every geolayer
 * needs a map layer at it's root. Otherwise they won't work-
 *
 * @export
 * @class MapLayer
 * @extends {GeoLayer}
 */
export class MapLayer extends GeoLayer {
    constructor(
        mapList,
        scatterContainer,
        displayObject,
        {
            onTransform = null,
            onChange = null,
            focus = null,
            zoom = null,
            viewport = null,
            name = null,
            mapChangeLocked = false
        } = {}
    ) {
        super(displayObject, {
            name
        })

        this.transformHandler = new EventHandler('onTransform', {
            listeners: onTransform
        })

        this.scatterContainer = scatterContainer

        this.changeHandler = new EventHandler('onChange', {
            listeners: onChange
        })

        this._mapview = new MapViewport({
            zoom,
            focus,
            viewport
        })

        this.mapList = mapList

        // //TODO Implement error handling here.
        // this.maps = maps
        // if (opts.map) this.placeMap(opts.map)
        this.dynamicElements = new Map()
        this._mapChangeLocked = mapChangeLocked

        // Binds the transformed callback beforehand.
        this.transformed = this.transformed.bind(this)

        this.changeMap(mapList.active)
    }

    //Todo: rename to mapviewport.
    /**
     * Returns the MapViewport of this map layer.
     *
     * @readonly
     * @member {MapViewport}
     * @memberof MapLayer
     */
    get mapview() {
        return this._mapview
    }

    get mapChangeLocked() {
        return this._mapChangeLocked
    }

    lockMapChange() {
        this._mapChangeLocked = true
    }

    unlockMapChange() {
        this._mapChangeLocked = false
    }

    /**
     * Adapts all child layers and their GeoGraphics.
     *
     * This is called primarily on a map change.
     *
     * @private
     * @memberof MapLayer
     */
    adapt() {
        this.layers.forEach(layer => {
            if (layer.adapt) layer.adapt(this.map)
        })
    }

    transformed(e) {
        this.mapview.update(this.map)
        this.layers.forEach(layer => layer.parentMapLayerTransformed(this))
        this.transformHandler.call(this)
    }

    /**
     * Clones the map layer-
     *
     * @param {ScatterContainer} scatterContainer - ScatterContainer of the app.
     * @param {PIXI.DisplayObject} [container=null] - Container of the newly created MapLayer. If null, an empty PIXI.Container will be created.
     * @returns {MapLayer} - Returns the cloned MapLayer.
     * @memberof MapLayer
     */
    clone(scatterContainer, container = null) {
        let mapList = this.mapList.clone()
        container = container == null ? new PIXI.Container() : container

        let mapLayerClone = new MapLayer(mapList, scatterContainer, container, {
            name: MapLayer.idx++,
            viewport: this.mapview.viewport,
            focus: this.mapview.focus,
            zoom: this.mapview.zoom,
            mapList
        })

        mapLayerClone.childrenVisibility = this.childrenVisibility
        return mapLayerClone
    }

    /**
     * Helper function to quickly display the next map.
     * Order is defined by the key ordering of the maplist.
     *
     * @memberof MapLayer
     */
    next() {
        let nextMap = this.mapList.next()
        this.changeMap(nextMap)
    }

    /**
     * Changes the map to the specified one, keeping the position and the zoom of the old map.
     *
     * @public
     * @param {GeoMap} map
     * @memberof MapLayer
     */
    changeMap(
        name
        /* map ,
        useScatterAsContainer = true // If set to false, the normal container is used. This is necessary when using submaps and the container need to be a RigidContainer.*/
    ) {
        if (!this.mapChangeLocked) {
            console.log('🗺️ Change map to: ', name)
            let oldMap = this.map

            this.mapList.select(name)

            if (oldMap) {
                oldMap.unload()
                oldMap.onTransform.remove(this.transformed)
            }

            let map = this.map
            if (map) {
                map.load()

                this.scatterContainer.addChild(map.image)

                this.mapview.apply(map)
                map.image.addChild(this.displayObject)

                // A geolayer's displayObject is on the parent layer.
                // A maplayer's displayobject is always the child of the map.
                this.adapt()

                this.changeHandler.call(this, map, oldMap)

                //Call transform one time manually.
                this.transformed()
                map.onTransform.add(this.transformed)
            } else {
                console.error(`Could not change map to ${name}.`)
            }
        }
    }

    /**
     * Applies the mapviews focus to the map.
     * This may be useful, if the container was modified.
     *
     * @memberof MapLayer
     */
    refocus() {
        this.mapview.apply(this.map)
    }

    /**
     * @public
     * @returns {GeoMap} - Returns the active map.
     * @readonly
     * @memberof MapLayer
     */
    get map() {
        return this.mapList.map
    }

    /**
     *
     * This is required for the geo layers.
     * MapLayer requests from the geoLayers traverse up to the next MapLayer.
     *
     * @public
     * @returns {MapLayer} - Returns this MapLayer.
     * @readonly
     * @memberof MapLayer
     */
    get mapLayer() {
        return this
    }

    /**
     * Cleans up the MapLayer.
     *
     * @public
     * @memberof MapLayer
     */
    cleanup() {
        this.mapList.cleanup()
    }
}

MapLayer.idx = 0