/*
 * Copyright 2013 Hannes Janetzek
 * Copyright 2016-2019 devemux86
 * Copyright 2016 Robin Boldt
 * Copyright 2017-2019 Gustl22
 *
 * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.oscim.layers.tile.buildings;

import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.Platform;
import org.oscim.core.MapElement;
import org.oscim.core.Tag;
import org.oscim.layers.Layer;
import org.oscim.layers.tile.MapTile;
import org.oscim.layers.tile.ZoomLimiter;
import org.oscim.layers.tile.vector.VectorTileLayer;
import org.oscim.layers.tile.vector.VectorTileLayer.TileLoaderThemeHook;
import org.oscim.map.Map;
import org.oscim.renderer.ExtrusionRenderer;
import org.oscim.renderer.OffscreenRenderer;
import org.oscim.renderer.OffscreenRenderer.Mode;
import org.oscim.renderer.bucket.ExtrusionBuckets;
import org.oscim.renderer.bucket.RenderBuckets;
import org.oscim.renderer.light.ShadowRenderer;
import org.oscim.theme.styles.ExtrusionStyle;
import org.oscim.theme.styles.RenderStyle;
import org.oscim.utils.geom.GeometryUtils;

import java.util.*;

public class BuildingLayer extends Layer implements TileLoaderThemeHook, ZoomLimiter.IZoomLimiter {

    protected static final int BUILDING_LEVEL_HEIGHT = 280; // cm

    public static final int MIN_ZOOM = 17;

    /**
     * Use Fast Approximate Anti-Aliasing (FXAA) and Screen Space Ambient Occlusion (SSAO).
     */
    public static boolean POST_AA = false;

    /**
     * Use real time calculations to pre-process data.
     */
    public static boolean RAW_DATA = false;

    /**
     * Let vanish extrusions / meshes which are covered by others.
     * {@link org.oscim.renderer.bucket.RenderBucket#EXTRUSION}: roofs are always translucent.
     * <p>
     * To better notice the difference, reduce the alpha value of extrusion colors in themes.
     */
    public static boolean TRANSLUCENT = true;

    private static final Object BUILDING_DATA = BuildingLayer.class.getName();

    // Can be replaced with Multimap in Java 8
    protected java.util.Map<Integer, List<BuildingElement>> mBuildings = new HashMap<>();

    protected final ExtrusionRenderer mExtrusionRenderer;

    private final ZoomLimiter mZoomLimiter;

    protected final VectorTileLayer mTileLayer;

    class BuildingElement {
        MapElement element;
        ExtrusionStyle style;

        BuildingElement(MapElement element, ExtrusionStyle style) {
            this.element = element;
            this.style = style;
        }
    }

    public BuildingLayer(Map map, VectorTileLayer tileLayer) {
        this(map, tileLayer, false, false);
    }

    public BuildingLayer(Map map, VectorTileLayer tileLayer, boolean mesh, boolean shadow) {
        this(map, tileLayer, MIN_ZOOM, map.viewport().getMaxZoomLevel(), mesh, shadow);
    }

    /**
     * @param map       The map data to add
     * @param tileLayer The vector tile layer which contains the tiles and the map elements
     * @param zoomMin   The minimum zoom at which the layer appears
     * @param zoomMax   The maximum zoom at which the layer appears
     * @param mesh      Declare if using mesh or polygon renderer
     * @param shadow    Declare if using shadow renderer
     */
    public BuildingLayer(Map map, VectorTileLayer tileLayer, int zoomMin, int zoomMax, boolean mesh, boolean shadow) {
        super(map);

        mTileLayer = tileLayer;
        mTileLayer.addHook(this);

        // Use zoomMin as zoomLimit to render buildings only once
        mZoomLimiter = new ZoomLimiter(tileLayer.getManager(), zoomMin, zoomMax, zoomMin);

        // Buildings translucency does not work on macOS, see #61
        if (CanvasAdapter.platform == Platform.MACOS)
            TRANSLUCENT = false;

        mRenderer = mExtrusionRenderer = new BuildingRenderer(tileLayer.tileRenderer(), mZoomLimiter, mesh, TRANSLUCENT);
        // TODO Allow shadow and POST_AA at same time
        if (shadow)
            mRenderer = new ShadowRenderer(mExtrusionRenderer);
        else if (POST_AA)
            mRenderer = new OffscreenRenderer(Mode.SSAO_FXAA, mRenderer);
    }

    @Override
    public void addZoomLimit() {
        mZoomLimiter.addZoomLimit();
    }

    @Override
    public void removeZoomLimit() {
        mZoomLimiter.removeZoomLimit();
    }

    /**
     * TileLoaderThemeHook
     */
    @Override
    public boolean process(MapTile tile, RenderBuckets buckets, MapElement element,
                           RenderStyle style, int level) {
        // FIXME artifacts at tile borders in last extraction zoom as they're clipped

        if (!(style instanceof ExtrusionStyle))
            return false;
        if (tile.zoomLevel > mZoomLimiter.getZoomLimit())
            return false;

        ExtrusionStyle extrusion = (ExtrusionStyle) style.current();

        // Filter all building elements
        // TODO #TagFromTheme: load from theme or decode tags to generalize mapsforge tags
        if (element.isBuilding() || element.isBuildingPart()) {
            List<BuildingElement> buildingElements = mBuildings.get(tile.hashCode());
            if (buildingElements == null) {
                buildingElements = new ArrayList<>();
                mBuildings.put(tile.hashCode(), buildingElements);
            }
            element = new MapElement(element); // Deep copy, because element will be cleared
            if (RAW_DATA && element.isClockwise() < 0) {
                // Buildings must be counter clockwise in VTM (mirrored to OSM)
                element.reverse();
            }
            buildingElements.add(new BuildingElement(element, extrusion));
            return true;
        }

        // Process other elements immediately
        processElement(element, extrusion, tile);

        return true;
    }

    /**
     * Process map element.
     *
     * @param element   the map element
     * @param extrusion the style of map element
     * @param tile      the tile which contains map element
     */
    protected void processElement(MapElement element, ExtrusionStyle extrusion, MapTile tile) {
        int height = 0; // cm
        int minHeight = 0; // cm

        Float f = element.getHeight(mTileLayer.getTheme());
        if (f != null)
            height = (int) (f * 100);
        else {
            // #TagFromTheme: generalize level/height tags
            String v = getValue(element, Tag.KEY_BUILDING_LEVELS);
            if (v != null)
                height = (int) (Float.parseFloat(v) * BUILDING_LEVEL_HEIGHT);
        }

        f = element.getMinHeight(mTileLayer.getTheme());
        if (f != null)
            minHeight = (int) (f * 100);
        else {
            // #TagFromTheme: level/height tags
            String v = getValue(element, Tag.KEY_BUILDING_MIN_LEVEL);
            if (v != null)
                minHeight = (int) (Float.parseFloat(v) * BUILDING_LEVEL_HEIGHT);
        }

        if (height == 0)
            height = extrusion.defaultHeight * 100;

        ExtrusionBuckets ebs = get(tile);
        ebs.addPolyElement(element, tile.getGroundScale(), extrusion.colors, height, minHeight);
    }

    /**
     * Process all stored map elements (here only buildings).
     *
     * @param tile the tile which contains stored map elements
     */
    protected void processElements(MapTile tile) {
        if (!mBuildings.containsKey(tile.hashCode()))
            return;

        List<BuildingElement> tileBuildings = mBuildings.get(tile.hashCode());
        Set<BuildingElement> rootBuildings = new HashSet<>();
        for (BuildingElement partBuilding : tileBuildings) {
            if (!partBuilding.element.isBuildingPart())
                continue;

            String refId = getValue(partBuilding.element, Tag.KEY_REF);
            if (refId == null)
                continue;

            // Search buildings which inherit parts
            for (BuildingElement rootBuilding : tileBuildings) {
                if (rootBuilding.element.isBuildingPart())
                    continue;
                if (RAW_DATA) {
                    float[] center = GeometryUtils.center(partBuilding.element.points, 0, partBuilding.element.pointNextPos, null);
                    if (!GeometryUtils.pointInPoly(center[0], center[1], rootBuilding.element.points, rootBuilding.element.index[0], 0))
                        continue;
                } else if (!(refId.equals(getValue(rootBuilding.element, Tag.KEY_ID))))
                    continue;

                rootBuildings.add(rootBuilding);
                break;
            }
        }

        tileBuildings.removeAll(rootBuildings); // root buildings aren't rendered

        for (BuildingElement buildingElement : tileBuildings) {
            processElement(buildingElement.element, buildingElement.style, tile);
        }
        mBuildings.remove(tile.hashCode());
    }

    /**
     * @param tile the MapTile
     * @return ExtrusionBuckets of the tile
     */
    public static ExtrusionBuckets get(MapTile tile) {
        ExtrusionBuckets ebs = (ExtrusionBuckets) tile.getData(BUILDING_DATA);
        if (ebs == null) {
            ebs = new ExtrusionBuckets(tile);
            tile.addData(BUILDING_DATA, ebs);
        }
        return ebs;
    }

    /**
     * Get the ExtrusionRenderer for customization.
     */
    public ExtrusionRenderer getExtrusionRenderer() {
        return mExtrusionRenderer;
    }

    /**
     * @return the tile source tag key or library tag key as fallback
     */
    protected String getKeyOrDefault(String key) {
        if (mTileLayer.getTheme() == null)
            return key;
        String res = mTileLayer.getTheme().transformBackwardKey(key);
        return res != null ? res : key;
    }

    /**
     * Get the forward transformed value from tile source tag via the library tag key.
     *
     * @param key the library tag key
     * @return the tile source tag value transformed to library tag value
     */
    protected String getTransformedValue(MapElement element, String key) {
        if (mTileLayer.getTheme() == null)
            return element.tags.getValue(key);
        /* Get tile source key of specified lib key from theme or fall back to lib key */
        key = getKeyOrDefault(key);
        /* Get element tag with tile source key, if exists */
        Tag tsTag = element.tags.get(key);
        if (tsTag == null)
            return null;
        /* Transform tile source tag to lib tag */
        Tag libTag = mTileLayer.getTheme().transformForwardTag(tsTag);
        if (libTag != null)
            return libTag.value;
        /* Use tile source value, if transformation rule not exists */
        return tsTag.value;
    }

    /**
     * Get the tile source tag value via the library tag key.
     *
     * @param key the library tag key
     * @return the tile source tag value of specified library tag key
     */
    protected String getValue(MapElement element, String key) {
        return element.tags.getValue(getKeyOrDefault(key));
    }

    @Override
    public void complete(MapTile tile, boolean success) {
        if (success) {
            processElements(tile);
            get(tile).prepare();
        } else
            get(tile).resetBuckets(null);
    }

    //    private int multi;
    //    @Override
    //    public void onInputEvent(Event event, MotionEvent e) {
    //        int action = e.getAction() & MotionEvent.ACTION_MASK;
    //        if (action == MotionEvent.ACTION_POINTER_DOWN) {
    //            multi++;
    //        } else if (action == MotionEvent.ACTION_POINTER_UP) {
    //            multi--;
    //            if (!mActive && mAlpha > 0) {
    //                // finish hiding
    //                //log.debug("add multi hide timer " + mAlpha);
    //                addShowTimer(mFadeTime * mAlpha, false);
    //            }
    //        } else if (action == MotionEvent.ACTION_CANCEL) {
    //            multi = 0;
    //            log.debug("cancel " + multi);
    //            if (mTimer != null) {
    //                mTimer.cancel();
    //                mTimer = null;
    //            }
    //        }
    //    }

}
