/*
 * Copyright (c) 2015 Samsung Electronics Co., Ltd. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*global window, document, tizen, setTimeout, setInterval, clearInterval,
console, webkitRequestAnimationFrame*/

window.app = window.app || {};

/**
 * App module.
 *
 * @module app
 * @namespace app
 */

(function defineApp(app) {
    'use strict';

    /**
     * Creates instance of Puzzle class.
     *
     * @memberof app
     * @private
     * @constructor
     */
    function Puzzle() {

        /**
         * Duration of changing piece position performed during shuffling
         * pieces (in milliseconds).
         *
         * @memberof Puzzle
         * @private
         * @const {number}
         */
        var SHUFFLE_TIME = 250,

            /**
             * Duration of piece animation performed during final
             * animation (in milliseconds).
             *
             * @memberof Puzzle
             * @private
             * @const {number}
             */
            SHORT_DELAY = 60,

            /**
             * Duration of piece animation performed during shuffling
             * animation (in milliseconds).
             *
             * @memberof Puzzle
             * @private
             * @const {number}
             */
            LONG_DELAY = 100,

            /**
             * Time of piece movement animation (in milliseconds).
             *
             * @memberof Puzzle
             * @private
             * @const {number}
             */
            ANIMATION_TIME = 400,

            /**
             *  Dictionary of remote controller key codes.
             *
             *  @memberof Puzzle
             *  @private
             *  @const {object}
             */
            KEY_CODES = {
                BACK: 10009,
                ENTER: 13,
                UP: 38,
                DOWN: 40,
                LEFT: 37,
                RIGHT: 39
            },

            /**
             * Identifier of interval action that changes pieces
             * position during shuffling.
             *
             * @memberof Puzzle
             * @private
             * @type {number}
             */
            interval = 0,

            /**
             * Index of last randomly chosen background image.
             *
             * @memberof Puzzle
             * @private
             * @type {number}
             */
            lastRandom = -1,

            /**
             * Names of available background images.
             *
             * @memberof Puzzle
             * @private
             * @type {string[]}
             */
            images = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg'],

            /**
             * Path to the current background image.
             *
             * @memberof Puzzle
             * @private
             * @type {string}
             */
            file = '',

            /**
             * Resolution of the screen.
             *
             * @memberof Puzzle
             * @private
             * @type {object}
             */
            resolution = {
                width: 1920,
                height: 1080
            },

            /**
             * Dimensions of the board:
             * - x: maximum count of pieces on horizontal axis,
             * - y: maximum count of pieces on vertical axis.
             *
             * @memberof Puzzle
             * @private
             * @type {object}
             */
            grid = {
                x: 6,
                y: 4
            },

            /**
             * Width of a piece.
             *
             * @memberof Puzzle
             * @private
             * @type {number}
             */
            pieceWidth = 0,

            /**
             * Height of a piece.
             *
             * @memberof Puzzle
             * @private
             * @type {number}
             */
            pieceHeight = 0,

            /**
             * Position of free space on the board.
             *
             * @memberof Puzzle
             * @private
             * @type {object}
             */
            free = {
                left: null,
                top: null
            },

            /**
             * Last moved piece reference.
             *
             * @memberof Puzzle
             * @private
             * @type {object}
             */
            lastPiece = null,

            /**
             * Indicates if user can move pieces or not.
             *
             * @memberof Puzzle
             * @private
             * @type {boolean}
             */
            lock = false,

            /**
             * Reference to the background element.
             *
             * @memberof Puzzle
             * @private
             * @type {HTMLElement}
             */
            backgroundEl = document.getElementById('background'),

            /**
             * Reference to the game board element.
             *
             * @memberof Puzzle
             * @private
             * @type {number}
             */
            gameEl = document.getElementById('game');

        /**
         * Returns random background image name.
         *
         * @memberof Puzzle
         * @private
         * @returns {string}
         */
        function randomFile() {
            var random = Math.floor(Math.random() * images.length);

            if (images.length === 1) {
                return images[0];
            }

            if (random === lastRandom) {
                return randomFile();
            }

            lastRandom = random;
            return images[random];
        }

        /**
         * Returns coordinates of piece position (in pixels).
         *
         * @memberof Puzzle
         * @private
         * @param {number} x
         * @param {number} y
         * @returns {object} position
         * @returns {number} position.left
         * @returns {number} position.top
         */
        function getPosition(x, y) {
            return {
                left: (x * pieceWidth),
                top: (y * pieceHeight)
            };
        }

        /**
         * Sets random file for background image.
         *
         * @memberof Puzzle
         * @private
         */
        function setFile() {
            file = 'images/' + randomFile();
        }

        /**
         * Animates an element.
         *
         * @memberof Puzzle
         * @private
         * @param {HTMLElement} el
         * @param {string} propertyName
         * @param {number} targetValue
         * @param {string} unit
         */
        function animateElement(el, propertyName, targetValue, unit) {
            var animationStartTime = +new Date(),
                startValue = parseFloat(el.style[propertyName], 10),
                diff = targetValue - startValue,
                tick;

            el.style[propertyName] = startValue + unit;

            tick = function tick() {
                // time passed since the start (in milliseconds)
                var currentAnimationTime = new Date() - animationStartTime,
                    // current progress (amount of animation which should be
                    // completed)
                    currentProgress = currentAnimationTime / ANIMATION_TIME;

                if (currentProgress >= 1) {
                    el.style[propertyName] = targetValue + unit;
                    return true;
                }

                el.style[propertyName] = (
                    startValue + currentProgress * diff) + unit;

                webkitRequestAnimationFrame(tick);
                return false;
            };

            tick();
        }

        /**
         * Applies the selected file as a background image for game board.
         *
         * @memberof Puzzle
         * @private
         */
        function setBackground() {
            backgroundEl.style.backgroundImage = 'url(' + file + ')';
            backgroundEl.style.display = 'block';
            backgroundEl.style.opacity = 1;
            animateElement(backgroundEl, 'opacity', 0.4, '');
            backgroundEl.style.webkitFilter = 'invert(100%)';
        }

        /**
         * Returns random piece adjacent to the free space.
         *
         * @memberof Puzzle
         * @private
         * @returns {HTMLElement}
         */
        function pickRandomNearest() {
            var nearest = [],
                nearestLen = 0,
                pieces = [],
                i = 0,
                last = Infinity,
                current = 0,

                // calculate the central point of the free tile
                position = getPosition(free.x, free.y),
                left = position.left + (pieceWidth / 2),
                top = position.top + (pieceHeight / 2);

            // find pieces adjacent to the free tile
            nearest.push(document.elementFromPoint(left, top - pieceHeight));
            nearest.push(document.elementFromPoint(left, top + pieceHeight));
            nearest.push(document.elementFromPoint(left - pieceWidth, top));
            nearest.push(document.elementFromPoint(left + pieceWidth, top));

            // remove empty values
            nearest = nearest.filter(function notEmpty(value) {
                return value;
            });

            nearestLen = nearest.length;
            for (i; i < nearestLen; i += 1) {
                current = nearest[i].step;
                if (nearest[i].classList.contains('piece') &&
                    nearest[i] !== lastPiece &&
                    Math.min(last, current) === current &&
                    (nearest[i].left !== free.left ||
                    nearest[i].top !== free.top)) {
                    pieces.push(nearest[i]);
                    last = nearest[i].step;
                }
            }
            lastPiece = pieces[Math.floor(Math.random() * pieces.length)];

            return lastPiece;
        }

        /**
         * Checks if a piece with given coordinates can be moved.
         * Returns true if it can be moved, false otherwise.
         *
         * @memberof Puzzle
         * @private
         * @param {number} x
         * @param {number} y
         * @returns {boolean}
         */
        function canMove(x, y) {
            var leftDiff = Math.abs(x - free.x),
                topDiff = Math.abs(y - free.y);

            return (leftDiff === 0 && topDiff === 1) ||
                (leftDiff === 1 && topDiff === 0);
        }

        /**
         * Sets free tile position.
         *
         * @memberof Puzzle
         * @private
         * @param {number} x
         * @param {number} y
         */
        function setFree(x, y) {
            free.x = x;
            free.y = y;
        }

        /**
         * Does the final fade-out piece animation.
         *
         * @memberof Puzzle
         * @private
         * @param {HTMLElement} piece
         */
        function finalPieceAnimation(piece) {
            animateElement(piece, 'width', pieceWidth * 2, 'px');
            animateElement(piece, 'width', pieceHeight * 2, 'px');
            animateElement(piece, 'left',
                parseInt(piece.style.left, 10) - (pieceWidth / 2), 'px');
            animateElement(piece, 'top',
                parseInt(piece.style.top, 10) - (pieceHeight / 2), 'px');
            animateElement(piece, 'opacity', 0, '');
        }

        /**
         * Schedules the final animation of the piece with given delay.
         * Returns created timer id.
         *
         * @memberof Puzzle
         * @private
         * @param {HTMLElement} piece
         * @param {number} delay
         * @returns {number} Timer id.
         */
        function scheduleFinalPieceAnimation(piece, delay) {
            return setTimeout(function onTimeout() {
                finalPieceAnimation(piece);
            }, delay);
        }

        /**
         * Does the final fade-out animation of the board.
         *
         * @memberof Puzzle
         * @private
         */
        function finalAnimation() {
            var pieces = document.getElementsByClassName('piece'),
                piecesLen = pieces.length;

            while (piecesLen > 0) {
                piecesLen -= 1;
                scheduleFinalPieceAnimation(
                    pieces[piecesLen],
                    piecesLen * SHORT_DELAY);
            }
        }

        /**
         * Removes all pieces from the board.
         *
         * @memberof Puzzle
         * @private
         */
        function clearGame() {
            var pieces = document.getElementById('pieces');

            lock = false;
            clearInterval(interval);
            while (pieces.firstChild) {
                pieces.removeChild(pieces.firstChild);
            }
        }

        /**
         * Checks if the piece position is correct.
         * If it does there is proper animation scheduled.
         *
         * @memberof Puzzle
         * @private
         * @param {HTMLElement} piece
         */
        function matchPosition(piece) {
            if (piece.x !== piece.startX || piece.y !== piece.startY) {
                return;
            }

            piece.classList.add('match');
            setTimeout(function animate() {
                var position = getPosition(piece.x, piece.y);

                // don't blink if the piece was already moved away
                if (position.left !== parseInt(piece.style.left, 10) ||
                    position.top !== parseInt(piece.style.top, 10)) {
                    return;
                }
                piece.style.opacity = 0;
                animateElement(piece, 'opacity', 1, '');
            }, ANIMATION_TIME + 50); // must wait for the animation to end
        }

        /**
         * Increments step count for the piece.
         *
         * @memberof Puzzle
         * @private
         * @param {HTMLElement} piece
         */
        function setStepCount(piece) {
            piece.step += 1;
        }

        /**
         * Handles piece position change.
         * Checks if piece position is correct, updates free tile position and
         * checks the winning condition.
         *
         * @memberof Puzzle
         * @private
         * @param {HTMLElement} piece
         * @param {number} left
         * @param {number} top
         */
        function onChangePiecePosition(piece, left, top) {
            matchPosition(piece);
            setStepCount(piece);
            setFree(left, top);
            checkWin();
            lock = false;
        }

        /**
         * Moves a specified piece into the free space.
         * If there is no piece specified, random piece is used.
         *
         * @memberof Puzzle
         * @private
         * @param {HTLMElement} [piece]
         */
        function changePiecePosition(piece) {
            var x = 0,
                y = 0,
                position = null;

            piece = piece || pickRandomNearest();
            x = piece.x;
            y = piece.y;
            if (!lock && canMove(x, y)) {
                lock = true;
                piece.classList.toggle('match', false);

                position = getPosition(free.x, free.y);

                // the piece goes into the free space
                piece.x = free.x;
                piece.y = free.y;
                animateElement(piece, 'left', position.left, 'px');
                animateElement(piece, 'top', position.top, 'px');

                onChangePiecePosition(piece, x, y);
            }
        }

        /**
         * Creates the board.
         *
         * @memberof Puzzle
         * @private
         */
        function createPieces() {
            var i = 0,
                j = 0,
                piece, position;

            for (i; i < grid.y; i += 1) {
                for (j = 0; j < grid.x; j += 1) {
                    if (i === (grid.y - 1) && j === (grid.x - 1)) {
                        // leave the last piece empty
                        break;
                    }

                    piece = document.createElement('div');
                    piece.className = 'piece match';
                    piece.x = j;
                    piece.y = i;
                    piece.startX = j;
                    piece.startY = i;
                    piece.number = i * grid.x + j;
                    piece.setAttribute('id', 'piece_' + j + '_' + i);
                    piece.step = 0;
                    piece.style.width = (pieceWidth - 1) + 'px';
                    piece.style.height = (pieceHeight - 1) + 'px';

                    position = getPosition(piece.x, piece.y);
                    piece.style.left = position.left + 'px';
                    piece.style.top = position.top + 'px';
                    piece.style.position = 'absolute';
                    piece.style.opacity = 1;
                    piece.style.backgroundSize = (grid.x * 100) + '%';
                    piece.style.backgroundImage = 'url(' + file + ')';
                    piece.style.backgroundPosition =
                        '-' + (piece.x * (pieceWidth - 1)) + 'px -' +
                        (piece.y * (pieceHeight - 1)) + 'px';

                    document.getElementById('pieces').appendChild(piece);
                }
            }
        }

        /**
         * Randomly shuffles all pieces.
         *
         * @memberof Puzzle
         * @private
         */
        function shufflePieces() {
            interval = setInterval(changePiecePosition, LONG_DELAY);
            setTimeout(function onTimeout() {
                clearInterval(interval);
                interval = 0;
            }, grid.x * grid.y * SHUFFLE_TIME);
        }

        /**
         * Starts the game.
         *
         * @memberof Puzzle
         * @public
         */
        function start() {
            clearGame();
            setFile();
            setBackground();
            setFree(grid.x - 1, grid.y - 1);
            createPieces();
            shufflePieces();
        }

        /**
         * Shows the end game notice.
         *
         * @memberof Puzzle
         * @private
         */
        function showSummary() {
            document.getElementById('info').style.display = 'block';
        }

        /**
         * Hides the end game notice.
         *
         * @memberof Puzzle
         * @private
         */
        function hideSummary() {
            document.getElementById('info').style.display = 'none';
        }

        /**
         * Checks the winning conditions (all pieces matched).
         *
         * If conditions are met, proper animation and summary are shown.
         *
         * @memberof Puzzle
         * @private
         */
        function checkWin() {
            if (document.querySelectorAll('.piece:not(.match)').length === 0) {
                finalAnimation();
                backgroundEl.style.webkitFilter = 'none';
                animateElement(backgroundEl, 'opacity', 1, '');
                showSummary();
                clearGame();
            }
        }

        /**
         * Set piece dimensions.
         *
         * @memberof Puzzle
         * @private
         */
        function setDimensions() {
            pieceWidth = Math.floor(resolution.width / grid.x);
            pieceHeight = Math.floor(resolution.height / grid.y);
        }

        /**
         * Sets height for the game element.
         *
         * @memberof Puzzle
         * @private
         */
        function gameElementAdjust() {
            gameEl.style.height = resolution.height + 'px';
            gameEl.style.width = resolution.width + 'px';
        }

        /**
         * Handles remote control buttons.
         *
         * @memberof Puzzle
         * @private
         * @param {KeyboardEvent} ev
         */
        function onKeyDown(ev) {
            var newTarget = {},
                targetPosition = {};

            if (ev.keyCode === KEY_CODES.BACK) {
                try {
                    tizen.application.getCurrentApplication().exit();
                } catch (error) {
                    console.error('exit application error', error);
                }
            }

            if (ev.keyCode === KEY_CODES.ENTER) {
                hideSummary();
                gameElementAdjust();
                setDimensions();
                start();
            } else {
                targetPosition = getPosition(free.x, free.y);

                switch (ev.keyCode) {
                case KEY_CODES.UP:
                    targetPosition.top -= pieceHeight;
                    break;
                case KEY_CODES.RIGHT:
                    targetPosition.left += pieceWidth;
                    break;
                case KEY_CODES.DOWN:
                    targetPosition.top += pieceWidth;
                    break;
                case KEY_CODES.LEFT:
                    targetPosition.left -= pieceHeight;
                    break;
                default:
                    // other keys are not supported
                    return;
                }

                newTarget = document.elementFromPoint(
                    targetPosition.left,
                    targetPosition.top
                );

                if (newTarget !== null &&
                    newTarget.classList.contains('piece')) {
                    changePiecePosition(newTarget);
                }
            }
        }

        /**
         * Registers event listeners.
         *
         * @memberof Puzzle
         * @private
         */
        function bindEvents() {
            document.addEventListener('keydown', onKeyDown);
        }

        /**
         * Initializes the game.
         *
         * @memberof Puzzle
         * @private
         */
        function init() {
            gameElementAdjust();
            setDimensions();
            bindEvents();
        }

        init();

        return {
            start: start
        };
    }

    /**
     * Starts the application.
     *
     * @memberof app
     * @public
     */
    app.run = function run() {
        new Puzzle().start();
    };

    app.run();
})(window.app);
