package fr.enssat.BoulderDash.models;

import fr.enssat.BoulderDash.exceptions.LevelConstraintNotRespectedException;
import fr.enssat.BoulderDash.exceptions.UnknownModelException;
import fr.enssat.BoulderDash.helpers.LevelLoadHelper;
import fr.enssat.BoulderDash.helpers.AudioLoadHelper;
import fr.enssat.BoulderDash.helpers.ModelConvertHelper;
import fr.enssat.BoulderDash.models.DisplayableElementModel;
import fr.enssat.BoulderDash.models.RockfordModel;
import fr.enssat.BoulderDash.models.GameInformationModel;
import fr.enssat.BoulderDash.models.SteelWallModel;
import fr.enssat.BoulderDash.models.EmptyModel;
import fr.enssat.BoulderDash.models.DiamondModel;
import fr.enssat.BoulderDash.models.DoorModel;
import fr.enssat.BoulderDash.models.DirtModel;
import fr.enssat.BoulderDash.models.ExpandingWallModel;
import fr.enssat.BoulderDash.models.CursorModel;

import java.awt.image.BufferedImage;
import java.util.Observable;


/**
 * LevelModel
 *
 * Levels are loaded from XML file. The view knows the model, the controller is
 * going to modify the model in function of the game panel. The model notifies
 * the view when there are changes on it.
 *
 * @author Colin Leverger <me@colinleverger.fr>
 * @since 2015-06-19
 */
public class LevelModel extends Observable implements Runnable {
	private DisplayableElementModel[][] groundGrid;
	private String levelName;
	private AudioLoadHelper audioLoadHelper;
	private int sizeWidth = 0;
	private int sizeHeight = 0;
	private int cursorXPosition = 0;
	private int cursorYPosition = 0;
	private boolean showCursor = false;
	private CursorModel cursorModel;
	private LevelLoadHelper levelLoadHelper;
	private RockfordModel rockford;
	private GameInformationModel gameInformationModel;
	private int rockfordPositionX, rockfordPositionY;
	private boolean gameRunning;
	private boolean gamePaused;
	// Are we in editor or game mode ?
	private String mode;

	/**
	 * Sprite animation thread
	 */
	private Thread spriteAnimator;

	/**
	 * Animation speed
	 */
	private final int DELAY = 25;

	/**
	 * Class constructor
	 *
	 * @param  levelName        Level name
	 * @param  audioLoadHelper  Audio load helper
     * @param  mode             Instance mode
	 */
	public LevelModel(String levelName, AudioLoadHelper audioLoadHelper, String mode) {
		this.levelName = levelName;
		this.audioLoadHelper = audioLoadHelper;
		this.gamePaused = false;
		this.gameRunning = true;
		this.mode = mode;

		this.levelLoadHelper = new LevelLoadHelper(this.levelName);

		this.groundGrid = this.levelLoadHelper.getGroundGrid();
		this.sizeWidth = this.levelLoadHelper.getWidthSizeValue();
		this.sizeHeight = this.levelLoadHelper.getHeightSizeValue();

		this.cursorModel = new CursorModel();
		this.gameInformationModel = new GameInformationModel(this.levelLoadHelper.getDiamondsToCatch());

		this.createLimits();

        if(this.mode.equals("game")) {
            this.initRockford();
            this.initThreadAnimator();
        }
	}

    /**
     * Class constructor
     *
     * @param  levelName        Level name
     * @param  audioLoadHelper  Audio load helper
     */
    public LevelModel(String levelName, AudioLoadHelper audioLoadHelper) {
        this(levelName, audioLoadHelper, "game");
    }

	/**
	 * Class constructor (editor mode)
	 *
	 * @param audioLoadHelper  Audio load helper
	 */
	public LevelModel(AudioLoadHelper audioLoadHelper) {
		this.audioLoadHelper = audioLoadHelper;
		this.gameRunning = false;
		this.mode = "editor";

		this.sizeWidth = 25 + 2;
		this.sizeHeight = 25 + 2;

		// Generate dirt
		this.groundGrid = new DisplayableElementModel[this.sizeWidth][this.sizeHeight];

		for (int x = 0; x < this.sizeWidth; x++) {
			for (int y = 0; y < this.sizeHeight; y++) {
				this.groundGrid[x][y] = new DirtModel();
			}
		}

		this.createLimits();
	}

	public String getLevelName() {
		return levelName;
	}

	/**
	 * Initializes the animator thread
	 */
	private void initThreadAnimator() {
		this.spriteAnimator = new Thread(this);
		this.spriteAnimator.start();
	}

	/**
	 * Initializes the Rockford position attributes
	 */
	private void initRockford() {
		this.rockfordPositionX = this.levelLoadHelper.getRockfordPositionX();
		this.rockfordPositionY = this.levelLoadHelper.getRockfordPositionY();
		this.rockford = this.levelLoadHelper.getRockfordInstance();
	}

	/**
	 * Creates the limits Puts steel walls all around the game panel
	 */
	private void createLimits() {
		int maxWidth = this.sizeWidth - 1;
		int maxHeight = this.sizeHeight - 1;

		for (int x = 0; x < this.sizeWidth; x++) {
			this.groundGrid[x][0] = new SteelWallModel();
			this.groundGrid[x][maxHeight] = new SteelWallModel();
		}
		for (int y = 0; y < this.sizeHeight; y++) {
			this.groundGrid[0][y] = new SteelWallModel();
			this.groundGrid[maxWidth][y] = new SteelWallModel();
		}
	}

	public void resetLevelModel() {
		this.groundGrid = this.levelLoadHelper.getGroundGrid();
		this.gameRunning = true;
		this.gameInformationModel.resetInformations();
	}

	/**
	 * Updates the horizontal & vertical positions of Rockford in the model
	 * 
	 * @param  posX  Horizontal position of Rockford
	 * @param  posY  Vertical position of Rockford
	 */
	public void updateRockfordPosition(int posX, int posY) {
		this.rockfordPositionY = posY;
		this.rockfordPositionX = posX;
	}

	/**
	 * Checks whether position is out-of-bounds or not
	 *
	 * @param  posX  Horizontal position
	 * @param  posY  Vertical position
	 */
	private boolean isOutOfBounds(int posX, int posY) {
		if (posX > 0 && posY > 0 && posX < this.getLevelLoadHelper().getHeightSizeValue() && posY < this.getLevelLoadHelper().getWidthSizeValue()) {
			return false;
		}

		return true;
	}

	/**
	 * Plays collision sound
	 */
	private void playCollisionSound(int posX, int posY) {
		String collisionSound = null;

		if (this.getRockford().isCollisionDone() == false) {
			// Out of bounds?
			if (this.isOutOfBounds(posX, posY) == true) {
				collisionSound = "touch";
			} else {
				DisplayableElementModel nextElement = this.groundGrid[posX][posY];
				collisionSound = nextElement.getCollideSound();
			}

			this.getRockford().setCollisionDone(true);
		}

		if (collisionSound != null) {
			this.audioLoadHelper.playSound(collisionSound);
		}
	}

	/**
	 * Gets the horizontal position of Rockford from the model
	 *
	 * @return  Horizontal position of Rockford
	 */
	public int getRockfordPositionX() {
		return this.rockfordPositionX;
	}

	/**
	 * Sets the new Rockford position
	 * 
	 * @param  posX  Next horizontal position on the grid
	 * @param  posY  Next vertical position on the grid
	 */
	public void setPositionOfRockford(int posX, int posY) {
		int oldX = this.getRockfordPositionX();
		int oldY = this.getRockfordPositionY();

		if (this.groundGrid[posX][posY].getSpriteName() == "diamond") {
			this.gameInformationModel.incrementScore();
			this.gameInformationModel.decrementRemainingsDiamonds();
			if (this.gameInformationModel.getRemainingsDiamonds() == 0) {
				this.spawnExit();
			}
		}
		if (this.groundGrid[posX][posY].getSpriteName() == "door") {
			this.gameRunning = false;
		}

		this.playCollisionSound(posX, posY);

		// Check that we are not out of bound...
		if (this.isOutOfBounds(posX, posY) == false) {
			// Create a new empty model in the old pos of Rockford
			this.groundGrid[oldX][oldY] = new EmptyModel();

			// Save the x / y pos of Rockford in the levelModel only
			this.updateRockfordPosition(posX, posY);

			this.groundGrid[posX][posY] = this.getRockford();
		}
	}

	/**
	 * When there is no more diamonds to catch, spawn a exit door randomly in
	 * the game
	 */
	private void spawnExit() {
		int x = (int) (Math.random() * (this.getSizeHeight() - 2));
		int y = (int) (Math.random() * (this.getSizeWidth() - 2));
		this.groundGrid[x + 1][y + 1] = new DoorModel();
	}

	/**
	 * Trigger block change with provided value
	 *
	 * @param  blockValue  New value
	 */
	public void triggerBlockChange(String blockValue) {
        // No block value?
        if(blockValue == null || blockValue.isEmpty()) {
            return;
        }

        // Cancel if Rockford is already in model
        if((blockValue.equals("Rockford") || blockValue.equals("rockford")) && this.isRockfordInModel()) {
            return;
        }

		// Grab model value
		ModelConvertHelper modelConverter = new ModelConvertHelper();
		DisplayableElementModel targetModel;
		int xPos, yPos;

		xPos = this.getCursorXPosition();
		yPos = this.getCursorYPosition();

		try {
			targetModel = modelConverter.toModel(blockValue, false);

            // Apply new model in place of cursor
            this.groundGrid[xPos + 1][yPos + 1] = targetModel;

            // Disable cursor (important)
            //this.setShowCursor(false);
		} catch (UnknownModelException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Gets the vertical position of Rockford from the model
	 *
	 * @return  Vertical position of Rockford
	 */
	public int getRockfordPositionY() {
		return this.rockfordPositionY;
	}

	/**
	 * Gets the Rockford object instance
	 *
	 * @return  Rockford object
	 */
	public RockfordModel getRockford() {
		return this.rockford;
	}

	/**
	 * Gets the displayable element at given positions
	 *
	 * @param  x  Block horizontal position
	 * @param  y  Block vertical position
	 * @return Displayable element at given positions
	 */
	public DisplayableElementModel getDisplayableElement(int x, int y) {
		return this.groundGrid[x][y];
	}

	/**
	 * Gets the image at given positions
	 *
	 * @param  x  Block horizontal position
	 * @param  y  Block vertical position
	 * @return Image at given positions
	 */
	public BufferedImage getImage(int x, int y) {
        DisplayableElementModel elementModel = this.getDisplayableElement(x, y);

        if(elementModel == null) {
            return new DirtModel().getSprite();
        }

		return elementModel.getSprite();
	}

	/**
	 * Gets the cursor image image
	 *
	 * @return  Cursor image
	 */
	public BufferedImage getCursorImage() {

		if (this.cursorModel == null) {
			this.cursorModel = new CursorModel();
		}

		return this.cursorModel.getSprite();
	}

    /**
     * Return whether rockford is in model or not
     * Notice: not optimized, be careful
     *
     * @return  Whether rockford is in model or not
     */
    public boolean isRockfordInModel() {
        boolean isInModel = false;

        // Iterate and catch it!
        for (int x = 0; x < this.getSizeWidth() && !isInModel; x++) {
            for (int y = 0; y < this.getSizeHeight() && !isInModel; y++) {
                if(this.groundGrid[x][y] != null && this.groundGrid[x][y].getSpriteName() == "rockford") {
                    isInModel = true;
                }
            }
        }

        return isInModel;
    }

    /**
     * Returns number of diamonds
     *
     * @return  Number of diamonds
     */
    public int countDiamonds() {
        int numberOfDiamonds = 0;

        // Iterate and catch it!
        for (int x = 0; x < this.getSizeWidth(); x++) {
            for (int y = 0; y < this.getSizeHeight(); y++) {
                if(this.groundGrid[x][y] != null && this.groundGrid[x][y].getSpriteName() == "diamond") {
                    numberOfDiamonds += 1;
                }
            }
        }

        return numberOfDiamonds;
    }

    /**
     * Returns whether constraints on model are respected or not
     */
    public void checkConstraints() throws LevelConstraintNotRespectedException {
        // Diamonds number?
        if(this.countDiamonds() < 3) {
            throw new LevelConstraintNotRespectedException("Add at least 3 diamonds!");
        }

        // Rockford in model?
        if(!this.isRockfordInModel()) {
            throw new LevelConstraintNotRespectedException("Add Rockford on the map!");
        }
    }

	/**
	 * Gets the level horizontal size
	 *
	 * @return   Horizontal size
	 */
	public int getSizeWidth() {
		return this.sizeWidth;
	}

	/**
	 * Sets the level horizontal size
	 *
	 * @param   sizeWidth  Horizontal size
	 */
	public void setSizeWidth(int sizeWidth) {
		this.sizeWidth = sizeWidth;
	}

	/**
	 * Gets the level vertical size
	 *
	 * @return  Vertical size
	 */
	public int getSizeHeight() {
		return this.sizeHeight;
	}

	/**
	 * Sets the level vertical size
	 *
	 * @return  sizeHeight  Vertical size
	 */
	public void setSizeHeight(int sizeHeight) {
		this.sizeHeight = sizeHeight;
	}

	/**
	 * Gets the ground level model
	 *
	 * @return Ground level model
	 */
	public DisplayableElementModel[][] getGroundLevelModel() {
		return groundGrid;
	}

	/**
	 * Notify observers about a model change
	 */
	private void localNotifyObservers() {
		this.notifyObservers();
		this.setChanged();
	}

	/**
	 * Update the current sprite Notifies the observers
	 * 
	 * @param  x  Sprite block horizontal position
	 * @param  y  Sprite block vertical position
	 */
	public void updateSprites(int x, int y) {
        if(groundGrid[x][y] == null) {
            groundGrid[x][y] = new DirtModel();
        }

        groundGrid[x][y].update(System.currentTimeMillis());
        this.localNotifyObservers();
	}

	/**
	 * Update all the sprites So that they can be animated
	 */
	public void run() {
		while (gameRunning) {
			if (!gamePaused) {
				for (int x = 0; x < this.getSizeWidth(); x++) {
					for (int y = 0; y < this.getSizeHeight(); y++) {
						this.updateSprites(x, y);
					}
				}
			}

			try {
				Thread.sleep(DELAY);
			} catch (InterruptedException e) {
				System.out.println("Interrupted: " + e.getMessage());
			}
		}
	}

	/**
	 * Increments the user score
	 */
	public void incrementScore() {
		this.gameInformationModel.incrementScore();
	}

	/**
	 * Gets the associated level load helper
	 *
	 * @return  Level load helper
	 */
	public LevelLoadHelper getLevelLoadHelper() {
		return this.levelLoadHelper;
	}

	/**
	 * Gets the cursor position X value
	 *
	 * @return  Cursor position X value
	 */
	public int getCursorXPosition() {
		return this.cursorXPosition;
	}

	/**
	 * Gets the cursor position Y value
	 *
	 * @return  Cursor position Y value
	 */
	public int getCursorYPosition() {
		return this.cursorYPosition;
	}

	/**
	 * Increaments the cursor position X value
	 *
	 * @return  Cursor position new X value
	 */
	public int incrementCursorXPosition() {
		if (this.cursorXPosition < (this.getSizeWidth() - 1 - 2)) {
			this.cursorXPosition = this.cursorXPosition + 1;
		}

		this.localNotifyObservers();
		return this.getCursorXPosition();
	}

	/**
	 * Decrements the cursor position X value
	 *
	 * @return  Cursor position new X value
	 */
	public int decrementCursorXPosition() {
		if (this.cursorXPosition > 0) {
			this.cursorXPosition = this.cursorXPosition - 1;
		}

		this.localNotifyObservers();
		return this.getCursorXPosition();
	}

	/**
	 * Increaments the cursor position Y value
	 *
	 * @return  Cursor position new Y value
	 */
	public int incrementCursorYPosition() {
		if (this.cursorYPosition < (this.getSizeHeight() - 1 - 2)) {
			this.cursorYPosition = this.cursorYPosition + 1;
		}

		this.localNotifyObservers();
		return this.getCursorYPosition();
	}

	/**
	 * Decrements the cursor position Y value
	 *
	 * @return  Cursor position new Y value
	 */
	public int decrementCursorYPosition() {
		if (this.cursorYPosition > 0) {
			this.cursorYPosition = this.cursorYPosition - 1;
		}

		this.localNotifyObservers();
		return this.getCursorYPosition();
	}

	/**
	 * sets the game to a defined state
	 *
	 * @param  gameRunning  Whether game is running or not
	 */
	public void setGameRunning(boolean gameRunning) {
		this.gameRunning = gameRunning;
		// Timer to refresh the view properly...
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.localNotifyObservers();
	}

	/**
	 * tells if the game is running
	 *
	 * @return  whether the game is running or not
	 */
	public boolean isGameRunning() {
		return gameRunning;
	}

	/**
	 * Gets whether cursor is to be shown or not
	 *
	 * @return  whether cursor needs to be shown or not
	 */
	public boolean getShowCursor() {
		return this.showCursor;
	}

	/**
	 * Sets whether cursor is to be shown or not
	 *
	 * @param   showCursor  whether cursor needs to be shown or not
	 */
	public void setShowCursor(boolean showCursor) {
		this.showCursor = showCursor;
	}

	/**
	 * When a boulder is falling on Rockford there is an explosion around him
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void exploseGround(int x, int y) {
		this.groundGrid[x][y] = new EmptyModel();
		this.groundGrid[x + 1][y] = new EmptyModel();
		this.groundGrid[x - 1][y] = new EmptyModel();
		this.groundGrid[x][y + 1] = new EmptyModel();
		this.groundGrid[x + 1][y + 1] = new EmptyModel();
		this.groundGrid[x - 1][y + 1] = new EmptyModel();
		this.groundGrid[x][y - 1] = new EmptyModel();
		this.groundGrid[x + 1][y - 1] = new EmptyModel();
		this.groundGrid[x - 1][y - 1] = new EmptyModel();
		this.rockford.setHasExplosed(true);

		// Again a sleep to notify the observers properly
		try {
			Thread.sleep(50);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.localNotifyObservers();
	}

	/**
	 * Makes the DisplayableElement[x][y] fall one box down
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void makeThisDisplayableElementFall(int x, int y) {
		this.groundGrid[x][y].setFalling(true);
		this.groundGrid[x][y + 1] = this.groundGrid[x][y];
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Makes the BoulderModel[x][y] slide left
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void makeThisBoulderSlideLeft(int x, int y) {
		this.groundGrid[x][y].setFalling(true);
		this.groundGrid[x - 1][y + 1] = this.groundGrid[x][y];
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Makes the BoulderModel[x][y] slide right
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void makeThisBoulderSlideRight(int x, int y) {
		this.groundGrid[x][y].setFalling(true);
		this.groundGrid[x + 1][y + 1] = this.groundGrid[x][y];
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Makes the BoulderModel[x][y] transform into a diamond
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void transformThisBoulderIntoADiamond(int x, int y) {
		this.groundGrid[x][y + 2] = new DiamondModel();
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Makes the BoulderModel[x][y] moving to right
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void moveThisBoulderToRight(int x, int y) {
		this.groundGrid[x + 1][y] = this.groundGrid[x][y];
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Makes the BoulderModel[x][y] moving to left
	 *
	 * @param  x  Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void moveThisBoulderToLeft(int x, int y) {
		this.groundGrid[x - 1][y] = this.groundGrid[x][y];
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Deletes the BoulderModel[x][y]
	 *
	 * @param  x   Object horizontal position
	 * @param  y  Object vertical position
	 */
	public void deleteThisBoulder(int x, int y) {
		this.groundGrid[x][y] = new EmptyModel();
	}

	/**
	 * Gets gameInformationModel
	 *
	 * @return gameInfos like score, remainings Diamonds etc
	 */
	public GameInformationModel getGameInformationModel() {
		return this.gameInformationModel;
	}

	/**
	 * Explose the brick wall
	 * 
	 * @param  x
	 * @param  y
	 */
	public void exploseThisBrickWall(int x, int y) {
		this.groundGrid[x][y] = new EmptyModel();
		this.groundGrid[x][y + 1] = new EmptyModel();
	}

	/**
	 * Expand the ExpandingWallModel to left
	 *
	 * @param  x
	 * @param  y
	 */
	public void expandThisWallToLeft(int x, int y) {
		this.groundGrid[x - 1][y] = new ExpandingWallModel();
	}

	/**
	 * Expand the ExpandingWallModel to right
	 * 
	 * @param  x
	 * @param  y
	 */
	public void expandThisWallToRight(int x, int y) {
		this.groundGrid[x + 1][y] = new ExpandingWallModel();
	}

	/**
	 * Set the gamePaused variable
	 * 
	 * @param  gamePaused
	 */
	public void setGamePaused(boolean gamePaused) {
		this.gamePaused = gamePaused;
	}

	/**
	 * Get the gamePaused variable
	 * 
	 * @return  gamePaused
	 */
	public boolean getGamePaused() {
		return this.gamePaused;
	}

	/**
	 * Get the mode where this levelModel is used
	 * 
	 * @return mode
	 */
	public String getMode() {
		return mode;
	}

	/**
	 * Set the mode where this levelModel is used
	 * 
	 * @param mode
	 */
	public void setMode(String mode) {
		this.mode = mode;
	}

}