/*
 * AbstractWorld.java
 *
 * Copyright (c) 2013 Martin Burkhard and Sonja Maier.
 * CSCM Cooperation Systems Center Munich, Institute for Software Technology.
 * Bundeswehr University Munich. All rights reserved.
 *
 * This program is made available under the terms of the
 * Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at <http://www.eclipse.org/legal/epl-v10.html>.
 *
 * The accompanying materials are made available under the terms
 * of Creative Commons Attribution-ShareAlike 3.0 Unported License.
 * You should have received a copy of the license along with this
 * work.  If not, see <http://creativecommons.org/licenses/by-sa/3.0/>.
 *
 *  Project: FishificationFX
 *   Author: Martin Burkhard
 *     Date: 9/2/13 11:26 PM
 */

package org.sociotech.unui.javafx.engine2d;

import com.google.common.io.Resources;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.TimelineBuilder;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.media.AudioClip;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.MarkerManager;
import org.sociotech.unui.javafx.engine2d.entities.Entity;
import org.sociotech.unui.javafx.engine2d.entities.EntityManager;
import org.sociotech.unui.javafx.engine2d.util.ScreenshotHelper;

import java.io.IOException;
import java.net.URL;

/**
 * Abstract 2D-Engine World for Sprite-based Entities.
 */
public abstract class AbstractWorld {

    // JavaFX Stage
    private final Stage m_worldStage;
    private       Scene m_worldCanvas;
    private       Group m_worldNodes;

    // Resources
    private EntityManager m_entityManager;
    private final ResourceManager<Image>     m_imageResourceManager = new ResourceManager<Image>();
    private final ResourceManager<AudioClip> m_audioResourceManager = new ResourceManager<AudioClip>();

    // Configuration
    private PropertiesConfiguration m_config = null;

    // Main Loop
    private boolean m_isPaused = false;
    private Timeline m_eventLoop;

    // FPS counter
    private double m_fps     = 0.0;
    private long   m_oldTime = 0;
    private int m_fpsCounter;

    // Text Font (default)
    private static String s_TextFontType = "Comic Sans MS";
    private static int    s_TextFontSize = 12;

    // Frames Per Second (FPS)
    private static boolean s_showFps = false;
    private Text m_fpsText;
    private static double s_fpsTextOffsetX = 90.0;
    private static double s_fpsTextOffsetY = 20.0;

    // Screen width and height
    private double m_width  = 0.0;
    private double m_height = 0.0;

    private final Logger m_log = LogManager.getLogger();

    protected AbstractWorld(String title, Stage worldStage) {
        m_worldStage = worldStage;
        m_worldStage.setTitle(title);
    }

    public void initialize(double width, double height) {

        m_width = width;
        m_height = height;

        // Init JavaFX Scene
        m_worldNodes = new Group();
        m_worldCanvas = new Scene(m_worldNodes, width, height);
        m_worldStage.setScene(m_worldCanvas);

        // Init Entity and Button Manager
        m_entityManager = new EntityManager(this);

        // Load configuration
        try {
            m_config = new PropertiesConfiguration("app.properties");
            loadConfiguration();

        } catch (ConfigurationException e) {
            m_log.warn(MarkerManager.getMarker("EXCEPTION"), "Error reading configuration file 'app.properties' from resources.", e);
            return;
        }

        // Initialize event loop
        initEventLoop();

        // Initialize derived class
        onInitialize();

        // Initialize FPS text
        initFpsText();
    }

    private void loadConfiguration() {

        try {
            s_TextFontType = m_config.getString("fishification.text.font.type");
            s_TextFontSize = m_config.getInt("fishification.text.font.size");
            s_showFps = m_config.getBoolean("fishification.fps.show");
            s_fpsTextOffsetX = m_config.getDouble("fishification.label.fps.offset.x");
            s_fpsTextOffsetY = m_config.getDouble("fishification.label.fps.offset.y");

            onLoadConfiguration(m_config);
        } catch (Exception e) {
            m_log.error(MarkerManager.getMarker("EXCEPTION"), "Error loading configuration.", e);
        }
    }

    protected abstract void onLoadConfiguration(PropertiesConfiguration config);

    protected synchronized PropertiesConfiguration getConfiguration() {
        return m_config;
    }

    private void initEventLoop() {
        final Duration fps = Duration.millis(1000 / (float) 60);

        final KeyFrame eventLoop = new KeyFrame(fps, new EventHandler<ActionEvent>() {

            public void handle(ActionEvent event) {

                // destroy entities on list
                getEntityManager().destroy();

                updateEntities();
                updateFps();

                onPostUpdate(m_fps);
            }

        });

        // Create Loop
        TimelineBuilder builder = TimelineBuilder.create();
        builder.cycleCount(Animation.INDEFINITE);
        builder.keyFrames(eventLoop);
        m_eventLoop = builder.build();
    }

    private void initFpsText() {
        m_fpsText = new Text();
        m_fpsText.setFont(Font.font(s_TextFontType, s_TextFontSize));
        m_fpsText.setX(getWorldCanvas().getWidth() - s_fpsTextOffsetX);
        m_fpsText.setY(s_fpsTextOffsetY);
        addNode(m_fpsText);
    }

    private void updateFps() {
        // Count frames
        m_fpsCounter++;

        long nanoTime = System.nanoTime();
        if (nanoTime > m_oldTime + 1000000000) {

            // Measure FPS
            m_fps = m_fpsCounter;

            m_fpsCounter = 0;
            m_oldTime = nanoTime;
        }
    }

    private synchronized void updateEntities() {
        for (Entity e : m_entityManager.getEntities()) {

            // Call update of every Entity
            e.update(getWorldCanvas(), e.getCurrentState(), m_fps);

            // Check collision with another Entity
            if (e.isCollidable()) {
                for (Entity e2 : m_entityManager.getEntities()) {
                    if (e2.isCollidable()) {

                        e.onCollisionWith(e2);
                    }
                }
            }
        }
    }

    protected abstract void onInitialize();

    private void onPostUpdate(double framesPerSecond) {

        if (s_showFps) {
            m_fpsText.setText(String.format("FPS: %.2f", framesPerSecond));
        }
    }

    public void launch() {
        m_eventLoop.play();
    }

    public void shutdown() {

        onShutdown();
        if (m_eventLoop != null) {
            m_eventLoop.stop();
            m_eventLoop = null;
        }

        if (m_entityManager != null) {
            m_entityManager.destroy();
            m_entityManager = null;
        }

        m_worldCanvas = null;
        m_worldNodes = null;
    }

    protected void onShutdown() {
    }

    public synchronized double getWidth() {
        return m_width;
    }

    public synchronized double getHeight() {
        return m_height;
    }

    public synchronized EntityManager getEntityManager() {
        return m_entityManager;
    }

    protected synchronized Scene getWorldCanvas() {
        return m_worldCanvas;
    }

    public synchronized Group getWorldNodes() {
        return m_worldNodes;
    }

    public synchronized boolean addNode(Node node) {
        return m_worldNodes.getChildren().add(node);
    }

    public synchronized boolean removeNode(Node node) {
        return m_worldNodes.getChildren().remove(node);
    }

    public synchronized ResourceManager<Image> getImageResourceManager() {
        return m_imageResourceManager;
    }

    protected synchronized void addImage(String imageName) {
        m_imageResourceManager.put(imageName, new Image(Resources.getResource("images/" + imageName).toExternalForm()));
    }

    protected synchronized void addImage(String imageName, int width, int height) {
        m_imageResourceManager.put(imageName,
                                   new Image(Resources.getResource("images/" + imageName).toExternalForm(), width,
                                             height, true, true));
    }

    synchronized ResourceManager<AudioClip> getAudioResourceManager() {
        return m_audioResourceManager;
    }

    protected void addAudioClip(String audioClipName) {
        URL bubbleAudioResource = Resources.getResource("sound/" + audioClipName);
        AudioClip bubbleAudioClip = new AudioClip(bubbleAudioResource.toExternalForm());
        getAudioResourceManager().put(audioClipName, bubbleAudioClip);
    }

    public void playAudioClip(String audioClipName, double volume) {
        // Play Audio
        ResourceManager<AudioClip> audioResourceManager = getAudioResourceManager();
        AudioClip audioClip = audioResourceManager.get(audioClipName);
        if (audioClip != null) {
            audioClip.play(volume);
        }
    }

    /**
     * Takes screenshot of the World's visible contents.
     *
     * @param path     Screenshot directory path
     * @param fileName Screenshot name added by number
     */
    protected synchronized void takeScreenshot(String path, String fileName) {
        try {
            ScreenshotHelper.takeScreenshot(m_worldCanvas, path, fileName);
        } catch (IOException e) {
            m_log.error(MarkerManager.getMarker("EXCEPTION"), "Error creating screenshot.", e);
        }
    }

    public synchronized boolean togglePause() {

        // toggle pause
        m_isPaused = !m_isPaused;

        if (m_isPaused) {
            m_eventLoop.pause();    // pause event loop
        } else {
            m_eventLoop.play();     // continue event loop
        }

        if (m_entityManager != null) {
            m_entityManager.pauseEntities(m_isPaused);
        }

        onPause(m_isPaused);

        return m_isPaused;
    }

    protected abstract void onPause(boolean isPaused);

    protected synchronized void toggleFps() {
        s_showFps = !s_showFps;
        if (!s_showFps) {
            m_fpsText.setText("");
        }
    }
}
