/*
 * Copyright (c) 2016 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 Image, document, setTimeout, window, THREE */

// create "app" namespace if not exists.
window.app = window.app || {
    pages: {}
};

/**
 * Menu page.
 *
 * @module menu
 * @requires lib/threejs
 * @requires {@link app}
 * @namespace app.pages.menu
 * @memberof app
 */

(function menuPage(app) {

    'use strict';

    /**
     * Images folder.
     *
     * @memberof menu
     * @private
     * @const {string}
     */
    var IMAGES_DIR = 'images/menu/',

        /**
         * Page selection animation time in milliseconds.
         *
         * @memberof menu
         * @private
         * @const {number}
         */
        ANIMATION_DURATION = 2000,

        /**
         * Page background color.
         *
         * @memberof menu
         * @public
         * @const {string}
         */
        BACKGROUND_COLOR = '#31bfb2',

        /**
         * Distance between vertical position of digits.
         *
         * @memberof menu
         * @private
         * @const {number}
         */
        DIGIT_LINE_HEIGHT = 150,

        /**
         * Meshes' vertical range.
         *
         * @memberof menu
         * @private
         * @const {number}
         */
        MESHES_VERTICAL_RANGE = {
            TOP: 160,
            BOTTOM: -90
        },

        /**
         * Meshes vertical animation speed factor.
         *
         * @memberof menu
         * @private
         * @const {number}
         */
        MESHES_VERTICAL_SPEED = 3.15,

        /**
         * Meshes' positions.
         *
         * @memberof menu
         * @private
         * @const {object[]}
         */
        MESHES_POSITIONS = [
            { x: 14, y: -33, z: -15 },
            { x: 20, y: 15, z: 0 },
            { x: -20, y: -90, z: -40 },
            { x: -25, y: 20, z: -40 },
            { x: 45, y: 65, z: -40 },
            { x: -105, y: -75, z: -130 },
            { x: 95, y: 0, z: -150 },
            { x: -10, y: 85, z: -150 }
        ],

        /**
         * ThreeJS Scene object.
         *
         * @memberof menu
         * @public
         * @type {THREE.Scene}
         */
        scene = new THREE.Scene(),

        /**
         * ThreeJS Camera object.
         *
         * @memberof menu
         * @public
         * @type {THREE.Camera}
         */
        camera = new THREE.PerspectiveCamera(85, 1, 0.1, 1250),

        /**
         * Pentagon object.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh}
         */
        pentagon = null,

        /**
         * Button 'go' object.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh}
         */
        goButton = null,

        /**
         * Floor object.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh}
         */
        floor = null,

        /**
         * Torus meshes.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh[]}
         */
        torusMeshes = [],

        /**
         * Mesh used for displaying zero digit.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh}
         */
        zero = null,

        /**
         * Array of meshes used for displaying page numbers.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh[]}
         */
        counterObjects = [],

        /**
         * Array of meshes used for displaying page labels.
         *
         * @memberof menu
         * @private
         * @type {THREE.Mesh[]}
         */
        labelsObjects = [],

        /**
         * Animation direction.
         * -1 backwards, 0 none, 1 forwards.
         *
         * @memberof menu
         * @private
         * @type {number}
         */
        animationDirection = 0,

        /**
         * Start animation time.
         *
         * @memberof menu
         * @private
         * @type {number}
         */
        startAnimTime = 0,

        /**
         * Current page number.
         *
         * @memberof menu
         * @private
         * @type {number}
         */
        currentPage = 1,

        /**
         * Defines textures sources for icons.
         *
         * @memberof menu
         * @private
         * @type {object[]} object
         * @type {string} object.src Filename
         * @type {HTMLImage} object.i Image element.
         */
        iconImages = [
            { src: 'icons/icon_1.png', i: null },
            { src: 'icons/icon_2.png', i: null },
            { src: 'icons/icon_3.png', i: null },
            { src: 'icons/icon_4.png', i: null },
            { src: 'icons/icon_5.png', i: null },
            { src: 'pentagon.png', i: null }
        ],

        /**
         * Images loading counter.
         *
         * @memberof menu
         * @private
         * @type {number}
         */
        loaderCounter = null,

        /**
         * Canvas used to fading icons textures.
         *
         * @memberof menu
         * @private
         * @type {HTMLElement}
         */
        iconCanvas = null,

        /**
         * Canvas context.
         *
         * @memberof menu
         * @private
         * @type {CanvasRenderingContext2D}
         */
        iconCanvasContext = null,

        /**
         * Texture for pentagonal element.
         *
         * @memberof menu
         * @private
         * @type {THREE.Texture}
         */
        pentagonBgTexture = null;

    /**
     * Creates and adds 'GO' button to the scene.
     *
     * @memberof menu
     * @private
     */
    function addButtonGo() {
        var texture = new THREE.TextureLoader().load(IMAGES_DIR + 'go.png');

        texture.repeat.set(0.55, 0.55);
        texture.offset.set(0.225, 0.225);

        goButton = new THREE.Mesh(
            new THREE.PlaneGeometry(24, 24, 1),
            new THREE.MeshBasicMaterial({
                map: texture,
                transparent: true,
                fog: false
            })
        );

        goButton.position.z = -40;
        goButton.position.x = -18;
        goButton.position.y = -28;
        scene.add(goButton);
    }

    /**
     * Adds floor object to the scene.
     *
     * @memberof menu
     * @private
     */
    function addFloor() {
        floor = new THREE.Mesh(
            new THREE.PlaneGeometry(1000, 150, 1),
            new THREE.MeshLambertMaterial({
                color: 0x0d4843,
                side: THREE.DoubleSide,
                depthTest: false
            })
        );

        floor.position.y = -45;
        floor.rotation.x = Math.PI / 2;

        scene.add(floor);
    }

    /**
     * Creates extruded, pentagonal object.
     *
     * @memberof menu
     * @private
     * @param {number} size
     * @param {number} depth
     * @returns {THREE.Mesh}
     */
    function Pentagon(size, depth) {
        var points = [],
            // configuration for THREE.ExtrudeGeometry
            extrudeSettings = {
                amount: depth,
                curveSegments: 1,
                steps: 1,
                material: 1,
                extrudeMaterial: 0,
                bevelEnabled: false
            },
            angle = 0,
            i = 0,
            shapeRotation = 0.11,
            // vertices count.
            pointsCount = 5,
            shape = null,
            geometry = null,
            material = null;

        // Calculate pentagon vertices positions.
        for (i = 0; i < pointsCount; i += 1) {
            angle = Math.PI * 2 / pointsCount * i - shapeRotation;
            points.push(new THREE.Vector2(
                Math.cos(angle) * size,
                Math.sin(angle) * size
            ));
        }

        // build a shape based on provided vertices.
        shape = new THREE.Shape(points);

        // create geometry based on provided shape.
        geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);

        // set canvas element as texture source.
        pentagonBgTexture = new THREE.Texture(iconCanvas);

        // set texture parameters.
        pentagonBgTexture.wrapS =
            pentagonBgTexture.wrapT = THREE.RepeatWrapping;

        pentagonBgTexture.repeat.set(0.002, 0.002);
        pentagonBgTexture.offset.set(0.485, 0.51);

        // initialize material.
        material = new THREE.MeshBasicMaterial({
            map: pentagonBgTexture,
            transparent: true
        });

        // create 3d object based on provided geometry and material.
        pentagon = new THREE.Mesh(geometry, material);
        pentagon.scale.set(1.1, 1.1, 1.1);
        return pentagon;
    }

    /**
     * Sets pentagon's properties and adds it to the scene.
     *
     * @memberof menu
     * @private
     */
    function addPentagon() {
        pentagon = new Pentagon(128, 4);
        pentagon.scale.set(0.2, 0.2, 0.2);
        pentagon.name = 'pentagon';
        pentagon.position.z = -65;
        pentagon.position.x = 20;
        pentagon.position.y = -6;

        scene.add(pentagon);
    }

    /**
     * Adds lights to the scene.
     * Ambient light and directional light.
     *
     * @memberof menu
     * @private
     */
    function addLight() {
        var ambientLight = new THREE.AmbientLight(0x888888),
            directLight = new THREE.DirectionalLight(0xffffff, 1.75);

        directLight.position.set(-10, 90, 0);

        scene.add(ambientLight);
        scene.add(directLight);
    }

    /**
     * Sets camera position.
     *
     * @memberof menu
     * @private
     */
    function setCameraPosition() {
        camera.position.z = 20;
    }

    /**
     * Adds fog to the scene.
     *
     * @memberof menu
     * @private
     */
    function addFog() {
        scene.fog = new THREE.Fog(0x31bfb2, 90, 10);
    }

    /**
     * Adds 3d objects with pages numbers.
     *
     * @memberof menu
     * @private
     */
    function addMenuCounters() {
        var PAGES_COUNT = 6,
            c = 0,
            obj = null,
            texture = null;

        for (c = 0; c < PAGES_COUNT; c += 1) {
            texture = new THREE.TextureLoader().load(
                IMAGES_DIR + 'numbers/' + c + '.png'
            );
            texture.repeat.set(0.4, 0.6);
            texture.offset.set(0.3, 0.20);

            obj = new THREE.Mesh(
                new THREE.PlaneGeometry(68, 105, 1),
                new THREE.MeshBasicMaterial({
                    side: THREE.DoubleSide,
                    map: texture,
                    transparent: true
                })
            );

            counterObjects.push(obj);
            scene.add(obj);

            obj.position.z = -120;
            obj.position.x = 20;
            obj.position.y = c * DIGIT_LINE_HEIGHT - 100;
        }

        zero = counterObjects[0];

        zero.position.z = -120;
        zero.position.y = 50;
        zero.position.x = -40;
    }

    /**
     * Adds 3d toruses to the scene.
     * Toruses are divided into three layers with different position.on z axis.
     *
     * @memberof menu
     * @private
     * @param {THREE.Mesh} object3d Base object to clone.
     */
    function addFlyingObjects(object3d) {
        // Object holding cloned torus.
        var newTorus = null;

        MESHES_POSITIONS.forEach(function createObjects(pos) {
            newTorus = object3d.clone();

            // Set random position (X, Y).
            newTorus.position.set(
                pos.x,
                pos.y,
                pos.z
            );

            // X axis initial rotation based on X position.
            newTorus.rotation.x = Math.sin(pos.x / 20) / 4;
            newTorus.rotation.z = 0.3;
            // Store initial rotation for use during animation.
            newTorus.orginalRotation = new THREE.Vector3(
                newTorus.rotation.x,
                0,
                0
            );

            newTorus.orginalPosition = newTorus.position;

            // Add object to scene.
            scene.add(newTorus);

            // Store object.
            torusMeshes.push(newTorus);
        });
    }

    /**
     * Adds objects with pages titles.
     *
     * @memberof menu
     * @private
     */
    function addMenuTitles() {
        var i = 0,
            object = null,
            texture = null;

        for (i = 0; i < app.TOTAL_PAGES; i += 1) {
            texture = new THREE.TextureLoader().load(
                IMAGES_DIR + 'labels/' + (i + 1) + '.png'
            );
            object = new THREE.Mesh(
                new THREE.PlaneGeometry(90, 90, 1),
                new THREE.MeshBasicMaterial({
                    map: texture,
                    transparent: true
                })
            );

            labelsObjects.push(object);
            object.position.z = -120;
            object.position.y = -8;
            object.position.x = -58 + i * 100;
            object.material.opacity = 0;
            scene.add(object);
        }

        labelsObjects[0].material.opacity = 1;
    }

    /**
     * Creates 3d objects and adds them to the scene.
     *
     * @memberof menu
     * @private
     */
    function createObjects() {
        var torus = new THREE.Mesh(
            new THREE.TorusGeometry(10, 1.3, 6, 5),
            new THREE.MeshLambertMaterial({
                color: 0xcfab61,
                alphaTest: 0.5,
                transparent: true,
                side: THREE.DoubleSide,
                depthWrite: false
            })
        );

        addFloor();
        addMenuTitles();
        addMenuCounters();
        addPentagon();
        addButtonGo();
        addFlyingObjects(torus);
    }

    /**
     * Draws texture used as pentagon texture.
     * Fades between two images dependly of current animation time.
     *
     * @memberof menu
     * @private
     * @param {Image} oldImage Image to fade out.
     * @param {Image} newImage Image to fade in.
     * @param {number} animationProgress Animation progress in range
     * from 0 to 1.
     */
    function pentagonTexture(oldImage, newImage, animationProgress) {
        iconCanvasContext.globalAlpha = 1;
        iconCanvasContext.drawImage(iconImages[5].image, 0, 0, 256, 256);
        iconCanvasContext.globalAlpha = 1 - animationProgress;
        iconCanvasContext.drawImage(oldImage, 0, 0, 128, 128, 40, 40, 176, 176);
        iconCanvasContext.globalAlpha = animationProgress;
        iconCanvasContext.drawImage(newImage, 0, 0, 128, 128, 40, 40, 176, 176);
        pentagonBgTexture.needsUpdate = true;
    }

    /**
     * Animates all 3d objects.
     *
     * @memberof menu
     * @public
     */
    function animate() {
        var DIGIT_VERTICAL_OFFSET = -100,
            LABEL_HORIZONTAL_SPACING = 250,
            LABEL_HORIZONAL_SHIFT = -58 + LABEL_HORIZONTAL_SPACING,
            animationTime = Date.now() - startAnimTime,
            normalizedAnimationTime = animationTime / ANIMATION_DURATION,
            signedNormalizedAnimationTime =
                normalizedAnimationTime * animationDirection,
            vShift = null,
            fadeToIconIndex = currentPage;

        if (!animationDirection) {
            return;
        }

        // calculate vertical shift for toruses.
        vShift = Math.sin(normalizedAnimationTime * Math.PI) *
            animationDirection;

        // move and rotate toruses.
        torusMeshes.forEach(function moveObject(mesh) {
            mesh.rotation.x = mesh.orginalRotation.x +
                signedNormalizedAnimationTime * Math.PI * 2;

            mesh.position.y = mesh.orginalPosition.y + vShift *
                MESHES_VERTICAL_SPEED;

            // Move object if out of boundary.
            if (mesh.position.y > MESHES_VERTICAL_RANGE.TOP) {
                mesh.position.y -= 240;
            }

            if (mesh.position.y < MESHES_VERTICAL_RANGE.BOTTOM) {
                mesh.position.y += 180;
            }
        });

        // rotate pentagon.
        pentagon.rotation.z =
            signedNormalizedAnimationTime * Math.PI * 2;

        // move and fade digits.
        counterObjects.forEach(function moveObject(obj, i) {
            var opacity =  0;

            // digit zero is not animated.
            if (!i) {
                return;
            }

            obj.position.y =
                DIGIT_LINE_HEIGHT *
                (currentPage - i + signedNormalizedAnimationTime + 1) +
                DIGIT_VERTICAL_OFFSET;

            if (i === currentPage) {
                opacity = 1 - normalizedAnimationTime;
            }

            if (animationDirection === 1) {
                if (i === (currentPage - 1)) {
                    opacity = 1 - normalizedAnimationTime;
                }

                if (i === (currentPage + 1) && animationDirection === 1) {
                    opacity = normalizedAnimationTime;
                }
            } else {
                if (i === (currentPage - 1)) {
                    opacity = normalizedAnimationTime;
                }

                if (i === (currentPage + 1) && animationDirection === 1) {
                    opacity = 1 - normalizedAnimationTime;
                }
            }

            obj.material.opacity = Math.min(
                1,
                opacity
            );
        });

        // move and fade labels.
        labelsObjects.forEach(function moveObject(obj, i) {
            obj.position.x = -(
                signedNormalizedAnimationTime - i + currentPage
            ) * LABEL_HORIZONTAL_SPACING + LABEL_HORIZONAL_SHIFT;

            obj.material.opacity = 1 - normalizedAnimationTime;
        });

        // set fading for number and label after and before current page.

        labelsObjects[currentPage - 1 + animationDirection].material.opacity =
            normalizedAnimationTime;

        // update texture for pentagon.
        if (animationDirection === -1) {
            fadeToIconIndex -= 2;
        }

        pentagonTexture(
            iconImages[currentPage - 1].image,
            iconImages[fadeToIconIndex].image,
            Math.abs(normalizedAnimationTime)
        );
    }

    /**
     * Sets additional canvas used to fading icons.
     *
     * @memberof menu
     * @private
     */
    function prepareIconCanvas() {
        iconCanvas = document.createElement('canvas');
        iconCanvas.classList.add('icon-canvas');
        document.querySelector('.application').appendChild(iconCanvas);
        iconCanvasContext = iconCanvas.getContext('2d');
        iconCanvas.width = iconCanvas.height = 256;
    }

    /**
     * Draws pentagon texture with first icon.
     * Called once on page initialization.
     *
     * @memberof menu
     * @private
     */
    function showFirstIcon() {
        pentagonTexture(iconImages[0].image, iconImages[0].image, 1);
    }

    /**
     * Loads image, when image is ready counter is decreased.
     * When counter reaches 0 showFirstIcon function is called.
     *
     * @memberof menu
     * @private
     * @param {object} imageObj
     */
    function loadImage(imageObj) {
        var i = new Image();

        i.onload = function onLoad() {
            imageObj.image = i;
            loaderCounter -= 1;
            if (!loaderCounter) {
                showFirstIcon();
            }
        };

        i.src = IMAGES_DIR + imageObj.src;
    }

    /**
     * Loads icons images.
     *
     * @memberof menu
     * @private
     */
    function preloadMenuImages() {
        loaderCounter = iconImages.length;
        iconImages.forEach(loadImage);
    }

    /**
     * Sets animation direction value to 0.
     * Changes selected page number to desired one using animation direction
     * value.
     *
     * @memberof menu
     * @private
     */
    function releaseAnimation() {
        currentPage += animationDirection;
        animationDirection = 0;
    }

    /**
     * Handles rotary event.
     * Does nothing when animation in progress.
     * Sets animation direction basing on rotary detent direction.
     * Sets delayed animation release.
     *
     * @memberof menu
     * @private
     * @param {Event} event
     */
    function onRotaryDetent(event) {
        if (animationDirection) {
            return;
        }

        animationDirection = 0;

        if (event.detail.direction === 'CW' && currentPage < app.TOTAL_PAGES) {
            animationDirection = 1;
        }

        if (event.detail.direction === 'CCW' && currentPage > 1) {
            animationDirection = -1;
        }

        if (animationDirection) {
            startAnimTime = Date.now();
            setTimeout(releaseAnimation, ANIMATION_DURATION);
        }
    }

    /**
     * Handles click event.
     * Changes application page if clicked bottom left screen quarter.
     *
     * @memberof menu
     * @private
     * @param {MouseEvent} e
     */
    function onClick(e) {
        // click on bottom left quarter of screen.
        var inRange = e.offsetX < app.DEVICE_RADIUS &&
            e.offsetY > app.DEVICE_RADIUS;

        if (!animationDirection && inRange) {
            app.changePage(currentPage);
        }
    }

    /**
     * Adds events listeners.
     *
     * @memberof menu
     * @private
     */
    function bindEvents() {
        document.addEventListener('click', onClick);
        document.addEventListener('rotarydetent', onRotaryDetent);
    }

    /**
     * Remove events listeners.
     *
     * @memberof menu
     * @private
     */
    function unbindEvents() {
        document.removeEventListener('click', onClick);
        document.removeEventListener('rotarydetent', onRotaryDetent);
    }

    /**
     * Binds events and shows additional renderer element.
     *
     * @memberof menu
     * @public
     */
    function onShow() {
        bindEvents();
    }

    /**
     * Unbinds events and hides additional renderer element.
     *
     * @memberof menu
     * public
     */
    function onHide() {
        unbindEvents();
    }

    /**
     * Initializes page.
     *
     * @memberof menu
     * private
     */
    function init() {
        prepareIconCanvas();
        addFog();
        addLight();
        createObjects();
        setCameraPosition();
        preloadMenuImages();

        app.changePage();
        app.startRendering();
    }

    app.pages.menu = {
        number: 0,
        onShow: onShow,
        onHide: onHide,
        name: 'menu',
        scene: scene,
        camera: camera,
        animate: animate,
        backgroundColor: BACKGROUND_COLOR
    };

    init();

}(window.app));
