/*
 * The MIT License (MIT)
 *
 * FXGL - JavaFX Game Library
 *
 * Copyright (c) 2015-2017 AlmasB ([email protected])
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.almasb.fxglgames.spaceinvaders;

import com.almasb.fxgl.app.ApplicationMode;
import com.almasb.fxgl.app.GameApplication;
import com.almasb.fxgl.app.GameSettings;
import com.almasb.fxgl.app.MenuItem;
import com.almasb.fxgl.core.math.FXGLMath;
import com.almasb.fxgl.entity.Entity;
import com.almasb.fxgl.entity.SpawnData;
import com.almasb.fxgl.input.Input;
import com.almasb.fxgl.input.UserAction;
import com.almasb.fxgl.ui.UI;
import com.almasb.fxglgames.spaceinvaders.collision.*;
import com.almasb.fxglgames.spaceinvaders.components.EnemyComponent;
import com.almasb.fxglgames.spaceinvaders.components.PlayerComponent;
import com.almasb.fxglgames.spaceinvaders.event.BonusPickupEvent;
import com.almasb.fxglgames.spaceinvaders.event.GameEvent;
import com.almasb.fxglgames.spaceinvaders.level.*;
import com.almasb.fxglgames.spaceinvaders.level.boss.BossLevel2;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.util.Duration;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

import static com.almasb.fxgl.dsl.FXGL.*;
import static com.almasb.fxglgames.spaceinvaders.Config.*;

/**
 * A clone of Space Invaders.
 *
 * @author Almas Baimagambetov (AlmasB) ([email protected])
 */
public class SpaceInvadersApp extends GameApplication {

    @Override
    protected void initSettings(GameSettings settings) {
        settings.setTitle("Space Invaders");
        settings.setVersion("1.0.1");
        settings.setWidth(WIDTH);
        settings.setHeight(HEIGHT);
        settings.setProfilingEnabled(false);
        settings.setMainMenuEnabled(false);
        settings.setGameMenuEnabled(true);
        settings.setIntroEnabled(false);
        settings.setEnabledMenuItems(EnumSet.of(MenuItem.EXTRA));
        settings.getCredits().addAll(Arrays.asList(
                "Music by Eric Matyas",
                "www.soundimage.org"
        ));
        settings.setApplicationMode(ApplicationMode.DEVELOPER);
    }

    @Override
    protected void initInput() {
        Input input = getInput();

        onKey(KeyCode.A, "Move Left", () -> playerComponent.left());
        onKey(KeyCode.D, "Move Right", () -> playerComponent.right());
        onKeyDown(KeyCode.F, "Red Laser", () -> playerComponent.shootRedLaser());
        onBtn(MouseButton.PRIMARY, "Shoot", () -> playerComponent.shoot());
        onBtn(MouseButton.SECONDARY, "Laser Beam", () -> playerComponent.shootLaser());

        // developer cheats
        if (getSettings().getApplicationMode() != ApplicationMode.RELEASE) {
            input.addAction(new UserAction("Next Level") {
                @Override
                protected void onActionBegin() {
                    nextLevel();
                }
            }, KeyCode.L);
        }
    }

    private Entity player;
    private PlayerComponent playerComponent;

    private int highScore;
    private String highScoreName;

    private SpaceInvadersController uiController;

    private List<SpaceLevel> levels;

    @Override
    protected void onPreInit() {
        getSettings().setGlobalSoundVolume(0.2);
        getSettings().setGlobalMusicVolume(0.5);

        loopBGM("bgm.mp3");

        onEvent(BonusPickupEvent.ANY, this::onBonusPickup);
        onEvent(GameEvent.ENEMY_KILLED, this::onEnemyKilled);
        onEvent(GameEvent.ENEMY_REACHED_END, this::onEnemyReachedEnd);
        onEvent(GameEvent.PLAYER_GOT_HIT, this::onPlayerGotHit);
    }

    private SaveData savedData = null;

    @Override
    protected void initGameVars(Map<String, Object> vars) {
        vars.put("score", 0);
        vars.put("level", 0);
        vars.put("lives", START_LIVES);
        vars.put("enemiesKilled", 0);
        vars.put("laserMeter", 0.0);
    }

    @Override
    protected void initGame() {
        getGameWorld().addEntityFactory(new SpaceInvadersFactory());

        // we have to use file system directly, since we are running without menus
        getFileSystemService().<SaveData>readDataTask(SAVE_DATA_NAME)
                .onSuccess(data -> savedData = data)
                .onFailure(ignore -> {})
                .run();

        initGame(savedData == null
                ? new SaveData("CPU", ACHIEVEMENT_MASTER_SCORER)
                : savedData);
    }

    @Override
    protected void initPhysics() {
        getPhysicsWorld().addCollisionHandler(new BonusPlayerHandler());
        getPhysicsWorld().addCollisionHandler(new BulletEnemyHandler());
        getPhysicsWorld().addCollisionHandler(new BulletPlayerHandler());
        getPhysicsWorld().addCollisionHandler(new BulletWallHandler());
        getPhysicsWorld().addCollisionHandler(new LaserBeamEnemyHandler());
    }

    private void initGame(SaveData data) {
        highScoreName = data.getName();
        highScore = data.getHighScore();

        levels = Arrays.asList(
                new Level20(),
                new Level15(),
                new Level2(),
                //new Level3(),
                new BossLevel2(),
                new Level4(),
                new Level5(),
                new Level16(),
                //new BossLevel2(),
                new Level7(),
                new Level8(),
                new Level9(),
                new Level11()
                //new BossLevel3()
        );

        spawnBackground();
        spawnPlayer();

        if (!runningFirstTime)
            nextLevel();
    }

    private void spawnBackground() {
        spawn("Background");

        spawn("Stars");
        
        run(() -> spawn("Meteor"), Duration.seconds(3));
    }

    private void spawnPlayer() {
        player = spawn("Player", getAppWidth() / 2 - 20, getAppHeight() - 40);
        playerComponent = player.getComponent(PlayerComponent.class);
    }

    private void spawnWall(double x, double y) {
        spawn("Wall", x, y);
    }

    private void spawnRandomBonus() {
        spawnBonus(FXGLMath.random(BonusType.values()).get());
    }

    private void spawnBonus(BonusType type) {
        double x = FXGLMath.random(0, getAppWidth() - 50);
        double y = FXGLMath.random(0, getAppHeight() / 3);

        getGameWorld().spawn("Bonus", new SpawnData(x, y).put("type", type));
    }

    private void nextLevel() {
        getInput().setProcessInput(false);

        if (geti("level") > 0) {
            cleanupLevel();
        }

        set("enemiesKilled", 0);
        inc("level", +1);

        if (geti("level") > levels.size()) {
            showGameOver();
            return;
        }

        playInCutscene(() -> {
            spawn("LevelInfo");

            runOnce(this::initLevel, Duration.seconds(LEVEL_START_DELAY));

            play(Asset.SOUND_NEW_LEVEL);
        });
    }

    private void initLevel() {
        getCurrentLevel().init();

        // TODO: move wall init to level so we can have walls in different places
        spawnWall(40, getAppHeight() - 100);
        spawnWall(120, getAppHeight() - 100);

        spawnWall(getAppWidth() - 160 - 40, getAppHeight() - 100);
        spawnWall(getAppWidth() - 80 - 40, getAppHeight() - 100);

        getInput().setProcessInput(true);
    }

    private void cleanupLevel() {
        getGameWorld().getEntitiesByType(
                SpaceInvadersType.ENEMY,
                SpaceInvadersType.BONUS,
                SpaceInvadersType.WALL,
                SpaceInvadersType.BULLET,
                SpaceInvadersType.LASER_BEAM)
                .forEach(Entity::removeFromWorld);

        getCurrentLevel().destroy();
    }

    private void playInCutscene(Runnable onFinished) {
        getCurrentLevel().playInCutscene(onFinished);
    }

    private void playOutCutscene(Runnable onFinished) {
        getCurrentLevel().playOutCutscene(onFinished);
    }

    private SpaceLevel getCurrentLevel() {
        // levels are counted 1, 2, 3 ...
        // list indices are   0, 1, 2 ...
        return levels.get(geti("level") - 1);
    }

    @Override
    protected void initUI() {
        uiController = new SpaceInvadersController(getGameScene());

        UI ui = getAssetLoader().loadUI(Asset.FXML_MAIN_UI, uiController);

        uiController.getLabelScore().textProperty().bind(getip("score").asString("Score: %d"));
        uiController.getLabelHighScore().setText("HiScore: " + highScore + " " + highScoreName + "");
        uiController.getLaserMeter().currentValueProperty().bind(getdp("laserMeter"));

        IntStream.range(0, geti("lives"))
                .forEach(i -> uiController.addLife());

        getGameScene().addUI(ui);
    }

    private boolean runningFirstTime = true;

    @Override
    protected void onUpdate(double tpf) {
        if (runningFirstTime) {
            nextLevel();
            getNotificationService().pushNotification("Music: Eric Matyas www.soundimage.org");
            runningFirstTime = false;
        }

        getCurrentLevel().onUpdate(tpf);
    }

    public void onPlayerGotHit(GameEvent event) {
        getGameScene().getViewport().shakeTranslational(9.5);

        inc("lives", -1);
        uiController.loseLife();

        playerComponent.enableInvincibility();

        runOnce(playerComponent::disableInvincibility, Duration.seconds(INVINCIBILITY_TIME));

        play(Asset.SOUND_LOSE_LIFE);

        if (geti("lives") == 0)
            showGameOver();
    }

    private int scoreForKill() {
        return SCORE_ENEMY_KILL * (getSettings().getGameDifficulty().ordinal() + SCORE_DIFFICULTY_MODIFIER);
    }

    public void onEnemyKilled(GameEvent event) {
        inc("enemiesKilled", +1);
        inc("score", scoreForKill());

        if (!playerComponent.isLaserBeamActive() && getd("laserMeter") < LASER_METER_MAX) {
            inc("laserMeter", +LASER_METER_RECHARGE);
            if (getd("laserMeter") > LASER_METER_MAX) {
                set("laserMeter", LASER_METER_MAX);
            }
        }

        if (FXGLMath.randomBoolean(BONUS_SPAWN_CHANCE)) {
            spawnRandomBonus();
        }

        if (getCurrentLevel().isFinished()) {
            playOutCutscene(this::nextLevel);
        }
    }

    public void onEnemyReachedEnd(GameEvent event) {
        inc("enemiesKilled", +1);

        inc("lives", -1);
        uiController.loseLife();

        if (geti("lives") == 0)
            showGameOver();

        if (geti("enemiesKilled") == ENEMIES_PER_LEVEL)
            nextLevel();
    }

    public void onBonusPickup(BonusPickupEvent event) {
        switch (event.getType()) {
            case ATTACK_RATE:
                playerComponent.increaseAttackSpeed(PLAYER_BONUS_ATTACK_SPEED);
                break;
            case LIFE:
                inc("lives", +1);
                uiController.addLife();
                break;
            case BOMB:
                killRandomEnemy();
                break;
        }
    }

    private void killRandomEnemy() {
        FXGLMath.random(getGameWorld().getEntitiesByType(SpaceInvadersType.ENEMY))
                .flatMap(e -> e.getComponentOptional(EnemyComponent.class))
                .ifPresent(e -> e.die());
    }

    private void showGameOver() {
        getDialogService().showConfirmationBox("Demo Over. Play Again?", yes -> {
            if (yes) {
                getGameWorld().getEntitiesCopy().forEach(Entity::removeFromWorld);
                getGameController().startNewGame();
            } else {

                int score = geti("score");

                if (score > highScore) {
                    getDialogService().showInputBox("High Score! Enter your name", playerName -> {

                        // we have to use file system directly, since we are running without menus
                        getFileSystemService().writeDataTask(new SaveData(playerName, score), SAVE_DATA_NAME).run();

                        getGameController().exit();
                    });
                } else {
                    getGameController().exit();
                }
            }
        });
    }

    public static void main(String[] args) {
        launch(args);
    }
}