/**
 * Silex, live web creation
 * http://projects.silexlabs.org/?/silex/
 *
 * Copyright (c) 2012 Silex Labs
 * http://www.silexlabs.org/
 *
 * Silex is available under the GPL license
 * http://www.silexlabs.org/silex/silex-licensing/
 */

import * as fs from 'fs';
import * as Path from 'path';
import { Constants } from '../../Constants';

// FIXME: path in constants
// const components = require('../../../dist/client/libs/prodotype/components/components.json')

/**
 * the version of the website is stored in the generator tag as "Silex v-X-Y-Z"
 * we get it from package.json
 * used for backward compat and for the static files URLs taken from //{{host}}/static/{{Y-Z}}
 */
const PACKAGE_JSON_DATA = JSON.parse(fs.readFileSync(Path.resolve(__dirname, '../../../../package.json')).toString());
const FRONT_END_VERSION = PACKAGE_JSON_DATA['version:frontend'].split('.').map((s) => parseInt(s));
const LATEST_VERSION = PACKAGE_JSON_DATA['version:backwardcompat'].split('.').map((s) => parseInt(s));

console.log(`\nSilex starts with backward compat version ${LATEST_VERSION} and front end version ${FRONT_END_VERSION}\n`);

/**
 * @fileoverview Handle backward compatibility when a user opens a site for edition
 *
 */
export default class BackwardCompat {
  constructor(private rootUrl: string) {}
  /**
   * handle backward compatibility issues
   * Backwardcompatibility process takes place after opening a file
   * @param {Document} doc
   * @return {Promise} a Promise, resolve can be called with a warning message
   */
  update(doc): Promise<string> {
    // fix an issue when the style tag has no type, then json is "broken"
    const styleTag = doc.querySelector('.' + Constants.JSON_STYLE_TAG_CLASS_NAME);
    if (styleTag) { styleTag.type = 'text/json'; } // old versions of silex have no json at all so do nothing in that case
    // if no generator tag, create one
    let metaNode = doc.querySelector('meta[name="generator"]');
    if (!metaNode) {
      metaNode = doc.createElement('meta');
      metaNode.setAttribute('name', 'generator');
      doc.head.appendChild(metaNode);
    }
    // retrieve the website version from generator tag
    const version = (metaNode.getAttribute('content') || '')
      .replace('Silex v', '')
      .split('.')
      .map((str) => parseInt(str, 10) || 0);

    const hasToUpdate = this.hasToUpdate(version, LATEST_VERSION);

    // warn the user
    if (this.amIObsolete(version, LATEST_VERSION)) {
      return Promise.resolve('This website has been saved with a newer version of Silex. Continue at your own risks.');
    } else if (this.hasToUpdate(version, [2, 2, 7])) {
      return Promise.reject({
        message: 'This website has been saved with an older version of Silex, which is not supported anymore as of March 2018. In order to convert it to a newer version, please go to <a href="https://old.silex.me">old.silex.me</a> to open and then save your website. <a href="https://github.com/silexlabs/Silex/wiki/Website-saved-with-older-version-of-Silex">More about this here</a>',
      });
    } else if (hasToUpdate) {
      const allActions = {};
      // convert to the latest version
      return this.to2_2_8(version, doc).then((actions8: string[]) => {
        if (actions8.length) { allActions['2.2.8'] = actions8; }
        return this.to2_2_9(version, doc).then((actions9: string[]) => {
        if (actions9.length) { allActions['2.2.9'] = actions9; }
        return this.to2_2_10(version, doc).then((actions10: string[]) => {
        if (actions10.length) { allActions['2.2.10'] = actions10; }
        // update the static scripts to match the current server and latest version
        this.updateStatic(doc);
        // store the latest version
        metaNode.setAttribute('content', 'Silex v' + LATEST_VERSION.join('.'));
        // build the report for the user
        const report = Object.keys(allActions).map((_version) => {
          return `<small>Update to version ${ _version }:
              <ul>${ allActions[_version].map((_action) => `<li class="no-list">${ _action }</li>`).join('') }</ul>
          </small>`;
        }).join('');
        // needs to reload if silex scripts and stylesheets have been updated
        return `<h2>Website updated</h2>
          <p>This website has been updated to Silex latest version.</p>
          <p>Before you save it, please check that everything is fine. Saving it with another name could be a good idea too (menu file > save as).</p>
          <small>Bellow you will find details of what I did.</small>
          ${ report }
        `;
      });
    });
  });
} else {
      // update the static scripts to match the current server URL
      this.updateStatic(doc);
      return Promise.resolve('');
    }
  }

  /**
   * update the static scripts to match the current server and latest version
   */
  updateStatic(doc) {
    // update //{{host}}/2.x/... to latest version
    const elements = doc.querySelectorAll('[data-silex-static]');
    for (const element of elements) {
      const propName = element.src ? 'src' : 'href';
      const newUrl = this.getStaticResourceUrl(element[propName]);
      const oldUrl = element.getAttribute(propName);
      if (oldUrl !== newUrl) {
        element.setAttribute(propName, newUrl);
      }
    }
  }

  /**
   * get the complete URL for the static file,
   * * on the current Silex server
   * * with the latest Silex version
   *
   * this will result in a URL on the current server, in the `/static/` folder
   *
   * @example `//localhost:6805/static/2.1/example/example.js` returns `//editor.silex.me/static/2.7/example/unslider.js`
   * @example `/static/2.1/example/example.js` returns `//editor.silex.me/static/2.7/example/example.js`
   *
   * with the current version
   * @param {string} url
   * @return {string}
   */
  getStaticResourceUrl(url) {
    const pathRelativeToStaticMatch = url.match(/static\/[0-9]*\.[0-9]*\/(.*)/);
    if (pathRelativeToStaticMatch == null) {
      console.warn('Error: could not extract the path and file name of static asset', url);
      return url;
    }
    const pathRelativeToStatic = pathRelativeToStaticMatch[1];
    return `${ this.rootUrl }/static/${ FRONT_END_VERSION[0] }.${ FRONT_END_VERSION[1] }/${ pathRelativeToStatic }`;
  }

  /**
   * check if the website has been edited with a newer version of Silex
   * @param {Array.<number>} initialVersion the website version
   * @param {Array.<number>} targetVersion  a given Silex version
   * @return {boolean}
   */
  amIObsolete(initialVersion, targetVersion) {
    return !!initialVersion[2] && initialVersion[0] > targetVersion[0] ||
      initialVersion[1] > targetVersion[1] ||
      initialVersion[2] > targetVersion[2];
  }

  /**
   * check if the website has to be updated for the given version of Silex
   * @param {Array.<number>} initialVersion the website version
   * @param {Array.<number>} targetVersion  a given Silex version
   * @return {boolean}
   */
  hasToUpdate(initialVersion, targetVersion) {
    return initialVersion[0] < targetVersion[0] ||
      initialVersion[1] < targetVersion[1] ||
      initialVersion[2] < targetVersion[2];
    }

    /**
     * @param {Array.<number>} version
     * @param {Document} doc
     * @return {Promise} a Promise
     */
    to2_2_8(version, doc): Promise<string[]> {
      return new Promise((resolve, reject) => {
        const actions = [];
        if (this.hasToUpdate(version, [2, 2, 8])) {
          // cleanup the hamburger menu icon
          const menuButton = doc.querySelector('.menu-button');
          if (menuButton) {
            menuButton.classList.remove('paged-element', 'paged-element-hidden', 'page-page-1', 'prevent-resizable');
            menuButton.classList.add('hide-on-desktop');
          }
          // give the hamburger menu a size (TODO: add to the json model too)
          doc.querySelector('.silex-inline-styles').innerHTML += '.silex-id-hamburger-menu {width: 50px;min-height: 40px;}';
          // pages need to have href set
          Array.from(doc.querySelectorAll('.page-element'))
          .forEach((el: HTMLLinkElement) => {
            el.setAttribute('href', '#!' + el.getAttribute('id'));
          });
          actions.push(`
            <p>I fixed the mobile menu so that it is compatible with the new publication (now multiple pages are generated instead of 1 single page for the whole website).</p>
          `);
        }
        resolve(actions);
      });
    }

    /**
     * @param {Array.<number>} version
     * @param {Document} doc
     * @return {Promise} a Promise
     */
    to2_2_9(version, doc): Promise<string[]> {
      return new Promise((resolve, reject) => {
        const actions = [];
        if (this.hasToUpdate(version, [2, 2, 9])) {
          // remove the hamburger menu icon
          const menuButton = doc.querySelector('.menu-button');
          if (menuButton) {
            menuButton.parentElement.removeChild(menuButton);
            actions.push(`
              <p>I removed the mobile menu as there is now a component for that.</p>
              <p><a target="_blank" href="https://github.com/silexlabs/Silex/wiki/Hamburger-menu">Read more about the Hamburger Menu component here</a>.</p>
            `);
          }
        }
        resolve(actions);
      });
    }
    /**
     * @param {Array.<number>} version
     * @param {Document} doc
     * @return {Promise} a Promise
     */
    to2_2_10(version, doc): Promise<string[]> {
      return new Promise((resolve, reject) => {
        const actions = [];
        if (this.hasToUpdate(version, [2, 2, 10])) {
          // the body is a drop zone, not selectable, not draggable, resizeable
          doc.body.classList.add(
            Constants.PREVENT_DRAGGABLE_CLASS_NAME,
            Constants.PREVENT_RESIZABLE_CLASS_NAME,
            Constants.PREVENT_SELECTABLE_CLASS_NAME);

          // each section background and foreground is a drop zone, not selectable, not draggable, resizeable
          const changedSections = Array.from(doc.querySelectorAll(`.${Constants.TYPE_SECTION}`)) as HTMLElement[];
          changedSections.forEach((el: HTMLElement) => el.classList.add(
            Constants.PREVENT_DRAGGABLE_CLASS_NAME,
            Constants.PREVENT_RESIZABLE_CLASS_NAME,
          ));

          // we add classes to the elements so that we can tell the stage component if an element is draggable, resizeable, selectable...
          const changedSectionsContent = Array.from(doc.querySelectorAll(`.${Constants.TYPE_SECTION}, .${Constants.TYPE_SECTION} .${Constants.TYPE_CONTAINER_CONTENT}`));
          changedSectionsContent.forEach((el: HTMLElement) => el.classList.add(
            Constants.PREVENT_DRAGGABLE_CLASS_NAME,
            // Constants.PREVENT_RESIZABLE_LEFT_CLASS_NAME,
            // Constants.PREVENT_RESIZABLE_RIGHT_CLASS_NAME
          ));
          actions.push(`Changed the body and ${changedSections.length} sections with new CSS classes to <a href="https://github.com/silexlabs/stage/" target="_blank">the new stage component.</a>`);

          // types are now with a "-element" suffix
          const changedElements = Array.from(doc.querySelectorAll(`[${Constants.TYPE_ATTR}]`));
          changedElements.forEach((el: HTMLElement) => el.setAttribute(Constants.TYPE_ATTR, el.getAttribute(Constants.TYPE_ATTR) + '-element'));

          actions.push(`Updated ${ changedElements.length } elements, changed their types to match the new version of Silex.`);
        }
        resolve(actions);
      });
    }
}
