// (C) Copyright 2015 Martin Dougiamas
//
// 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.

import { CoreAppProvider } from '@providers/app';
import { CoreFileProvider } from '@providers/file';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { Observable, Observer } from 'rxjs';
import { InAppBrowserEvent } from '@ionic-native/in-app-browser';

/**
 * Emulates the Cordova InAppBrowserObject in desktop apps.
 * We aren't extending InAppBrowserObject because its constructor also opens a window, so we'd end up with 2 windows.
 */
export class InAppBrowserObjectMock {
    protected window;
    protected browserWindow;
    protected screen;
    protected isSSO: boolean;

    constructor(appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private urlUtils: CoreUrlUtilsProvider,
            private url: string, target?: string, options: string = 'location=yes') {

        if (!appProvider.isDesktop()) {
            // This plugin is only supported in desktop.
            return;
        }

        this.browserWindow = require('electron').remote.BrowserWindow;
        this.screen = require('electron').screen;
        this.isSSO = !!(url && url.match(/\/launch\.php\?service=.+&passport=/));

        let width = 800,
            height = 600,
            display;

        const bwOptions: any = {
                webPreferences: {}
            };

        try {
            // Create a separate session for inappbrowser so we can clear its data without affecting the app.
            bwOptions.webPreferences.session = require('electron').remote.session.fromPartition('inappbrowsersession');
        } catch (ex) {
            // Ignore errors.
        }

        if (screen) {
            display = this.screen.getPrimaryDisplay();
            if (display && display.workArea) {
                width = display.workArea.width || width;
                height = display.workArea.height || height;
            }
        }

        // Create the BrowserWindow options based on the received options.
        bwOptions.width = width;
        bwOptions.height = height;
        if (options.indexOf('hidden=yes') != -1) {
            bwOptions.show = false;
        }
        if (options.indexOf('location=no') != -1) {
            bwOptions.frame = false;
        }
        if (options.indexOf('fullscreen=yes') != -1) {
            bwOptions.fullscreen = true;
        }
        if (options.indexOf('clearsessioncache=yes') != -1 && bwOptions.webPreferences.session) {
            bwOptions.webPreferences.session.clearStorageData({storages: 'cookies'});
        }

        this.window = new this.browserWindow(bwOptions);
        this.window.loadURL(url);

        if (this.isSSO) {
            // SSO in Linux. Simulate it's an iOS device so we can retrieve the launch URL.
            // This is needed because custom URL scheme is not supported in Linux.
            const userAgent = 'Mozilla/5.0 (iPad) AppleWebKit/603.3.8 (KHTML, like Gecko) Mobile/14G60';
            this.window.webContents.setUserAgent(userAgent);
        }
    }

    /**
     * Close the window.
     */
    close(): void {
        if (this.window.isDestroyed()) {
            // Window already closed, nothing to do.
            return;
        }

        try {
            this.window.close();
        } catch (ex) {
            // Ignore errors.
        }
    }

    /**
     * Execute a JS script.
     *
     * @param {any} details Details of the script to run, specifying either a file or code key.
     * @return {Promise<any>} Promise resolved when done.
     */
    executeScript(details: any): Promise<any> {
        return new Promise((resolve, reject): void => {
            if (details.code) {
                this.window.webContents.executeJavaScript(details.code, false, resolve);
            } else if (details.file) {
                this.fileProvider.readFile(details.file).then((code) => {
                    this.window.webContents.executeJavaScript(code, false, resolve);
                }).catch(reject);
            } else {
                reject('executeScript requires exactly one of code or file to be specified');
            }
        });
    }

    /**
     * Recursive function to get the launch URL from the contents of a BrowserWindow.
     *
     * @param {number} [retry=0] Retry number.
     * @return {Promise<string>} Promise resolved with the launch URL.
     */
    protected getLaunchUrl(retry: number = 0): Promise<string> {

        if (this.window.isDestroyed()) {
            // Window is destroyed, stop.
            return Promise.reject(null);
        }

        return new Promise((resolve, reject): void => {
            // Execute Javascript to retrieve the launch link.
            const jsCode = 'var el = document.querySelector("#launchapp"); el && el.href;';
            let found = false;
            this.window.webContents.executeJavaScript(jsCode).then((launchUrl) => {
                found = true;
                resolve(launchUrl);
            });

            setTimeout(() => {
                if (found) {
                    // URL found, stop.
                } else if (retry > 5) {
                    // Waited enough, stop.
                    reject();
                } else {
                    this.getLaunchUrl(retry + 1).then(resolve, reject);
                }
            }, 300);

        });
    }

    /**
     * Hide the window.
     */
    hide(): void {
        this.window.hide();
    }

    /**
     * Insert CSS.
     *
     * @param {any} details Details of the CSS to insert, specifying either a file or code key.
     * @return {Promise<any>} Promise resolved when done.
     */
    insertCSS(details: any): Promise<any> {
        return new Promise((resolve, reject): void => {
            if (details.code) {
                this.window.webContents.insertCSS(details.code);
                resolve();
            } else if (details.file) {
                this.fileProvider.readFile(details.file).then((code) => {
                    this.window.webContents.insertCSS(code);
                    resolve();
                }).catch(reject);
            } else {
                reject('insertCSS requires exactly one of code or file to be specified');
            }
        });
    }

    /**
     * Listen to events happening.
     *
     * @param {string} name Name of the event.
     * @return {Observable<InAppBrowserEvent>} Observable that will listen to the event on subscribe, and will stop listening
     *                                         to the event on unsubscribe.
     */
    on(name: string): Observable<InAppBrowserEvent> {
        // Create the observable.
        return new Observable<InAppBrowserEvent>((observer: Observer<InAppBrowserEvent>): any => {
            // Helper functions to handle events.
            const received = (event, url): void => {
                    try {
                        event.url = url || (this.window.isDestroyed() ? '' : this.window.getURL());
                        event.type = name;
                        observer.next(event);
                    } catch (ex) {
                        // Ignore errors.
                    }
                },
                finishLoad = (event): void => {
                    // Check if user is back to launch page.
                    if (this.urlUtils.removeUrlParams(this.url) == this.urlUtils.removeUrlParams(this.window.getURL())) {
                        // The launch page was loaded. Search for the launch link.
                        this.getLaunchUrl().then((launchUrl) => {
                            if (launchUrl) {
                                // Launch URL retrieved, send it and stop listening.
                                received(event, launchUrl);
                            }
                        });
                    }
                };

            switch (name) {
                case 'loadstart':
                    this.window.webContents.on('did-start-loading', received);

                    if (this.isSSO) {
                        // Linux doesn't support custom URL Schemes. Check if launch page is loaded.
                        this.window.webContents.on('did-finish-load', finishLoad);
                    }
                    break;

                case 'loadstop':
                    this.window.webContents.on('did-finish-load', received);
                    break;

                case 'loaderror':
                    this.window.webContents.on('did-fail-load', received);
                    break;
                case 'exit':
                    this.window.on('close', received);
                    break;
                default:
            }

            return (): void => {
                // Unsubscribing. We need to remove the listeners.
                switch (name) {
                    case 'loadstart':
                        this.window.webContents.removeListener('did-start-loading', received);
                        this.window.webContents.removeListener('did-finish-load', finishLoad);
                        break;

                    case 'loadstop':
                        this.window.webContents.removeListener('did-finish-load', received);
                        break;

                    case 'loaderror':
                        this.window.webContents.removeListener('did-fail-load', received);
                        break;

                    case 'exit':
                        this.window.removeListener('close', received);
                        break;
                    default:
                }
            };
        });
    }

    /**
     * Show the window.
     */
    show(): void {
        this.window.show();
    }
}
