'use strict';

const fs = require('fs');

const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/u;


// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
  meta: {
    type: 'layout',
    docs: {
      description: 'Validate the presence of a license header',
      category: 'Possible Errors',
      recommended: true
    },
    fixable: true,
    schema: [
      {
        type: [ 'string', 'array' ],
        items: {
          type: 'string'
        }
      }
    ]
  },

  create: function(context) {

    const sourceCode = context.getSourceCode();

    const newlineChar = getNewlineCharacter(sourceCode);

    const separator = `${newlineChar}${newlineChar}`;

    const {
      licenseHeader: rawHeader
    } = parseOptions(context);

    const licenseHeader = replaceNewlines(rawHeader, newlineChar);


    // ----------------------------------------------------------------------
    // Helpers
    // ----------------------------------------------------------------------

    function getNewlineCharacter(sourceCode) {
      const match = LINEBREAK_MATCHER.exec(sourceCode.getText());

      return match && match[0] || '\n';
    }

    function readLicenseFile(path) {

      if (!path) {
        throw new Error('missing license header path');
      }

      try {
        return fs.readFileSync(path, 'utf-8');
      } catch (e) {
        throw new Error(`could not read license header from <${path}>`);
      }
    }

    function replaceNewlines(text, newlineChar) {
      return text.replace(createGlobalLinebreakMatcher(), newlineChar);
    }

    function parseOptions(context) {

      const {
        options
      } = context;

      const [ pathOrLicense ] = options;

      let licenseHeader;

      if (Array.isArray(pathOrLicense)) {

        // If the path is an array, that means simply use each element as a line for the header
        licenseHeader = pathOrLicense.join('\n');
      } else {
        licenseHeader = readLicenseFile(pathOrLicense).trim();
      }

      return {
        licenseHeader
      };
    }

    function isShebang(node) {
      return node.type === 'Shebang';
    }

    function isLicenseHeader(node) {
      return /(Copyright|@license|SPDX-License-Identifier)\b/i.test(node.value);
    }

    function hasValidText(comment) {
      return sourceCode.getText(comment) === licenseHeader;
    }

    function getLeadingComments(node) {

      if (node.type === 'Program') {
        return sourceCode.getCommentsInside(node);
      } else {
        return sourceCode.getCommentsBefore(node);
      }
    }

    function findLicenseComment(comments) {

      return comments.reduce(function(found, comment) {

        if (found !== null) {
          return found;
        }

        if (isLicenseHeader(comment)) {
          return comment;
        }

        // continue searching, if shebang is found
        return isShebang(comment) ? null : false;
      }, null);
    }

    // ----------------------------------------------------------------------
    // Public
    // ----------------------------------------------------------------------

    return {
      Program: function(programNode) {

        // ignore Vue SFC
        if (programNode.templateBody) {
          return;
        }

        const firstStatement = programNode.body[0];

        // ignore Svelte SFC
        if (/^Svelte*/.test(firstStatement && firstStatement.type)) {
          return;
        }

        const comments = getLeadingComments(firstStatement || programNode);

        const licenseNode = findLicenseComment(comments);

        if (licenseNode) {
          if (!hasValidText(licenseNode)) {
            return context.report({
              node: licenseNode,
              message: 'Invalid license header',
              fix: function(fixer) {
                return fixer.replaceText(licenseNode, licenseHeader);
              }
            });
          }

          const previousSibling = comments[comments.indexOf(licenseNode) - 1];

          const nextSibling = comments[comments.indexOf(licenseNode) + 1] || firstStatement;

          if (nextSibling) {
            const lLocEnd = licenseNode.loc.end.line;
            const nLocStart = nextSibling.loc.start.line;

            const fix = function(fixer) {
              return fixer.replaceTextRange(
                [ licenseNode.range[1], nextSibling.range[0] ],
                separator
              );
            };

            if (nLocStart - lLocEnd > 2) {
              context.report({
                node: licenseNode,
                loc: licenseNode.loc.end,
                message: 'Superfluous new lines after license header',
                fix
              });
            }

            if (nLocStart - lLocEnd < 2) {
              context.report({
                node: licenseNode,
                loc: licenseNode.loc.end,
                message: 'Missing new line after license header',
                fix
              });
            }
          }


          if (previousSibling) {
            const lLocStart = licenseNode.loc.start.line;
            const nLocEnd = previousSibling.loc.end.line;

            const fix = function(fixer) {
              return fixer.replaceTextRange(
                [ previousSibling.range[1], licenseNode.range[0] ],
                separator
              );
            };

            if (lLocStart - nLocEnd > 2) {
              context.report({
                node: licenseNode,
                loc: licenseNode.loc.start,
                message: 'Superfluous new lines before license header',
                fix
              });
            }

            if (lLocStart - nLocEnd < 2) {
              context.report({
                node: licenseNode,
                loc: licenseNode.loc.start,
                message: 'Missing new line before license header',
                fix
              });
            }
          } else {
            const lLocStart = licenseNode.loc.start.line;

            if (lLocStart !== 1) {
              context.report({
                node: licenseNode,
                loc: licenseNode.loc.start,
                message: 'Superfluous new lines before license header',
                fix: function(fixer) {
                  return fixer.replaceTextRange([ 0, licenseNode.range[0] ], '');
                }
              });
            }
          }
        } else {
          const shebang = comments.find(isShebang);

          context.report({
            node: shebang || comments[0] || firstStatement || programNode,
            message: 'Missing license header',
            fix: function(fixer) {
              if (shebang) {
                return fixer.insertTextAfter(
                  shebang,
                  `${separator}${licenseHeader}`
                );
              } else {

                const beforeNode = comments[0] || firstStatement;

                const endRange = beforeNode ? beforeNode.range[0] : programNode.range[1];

                return fixer.replaceTextRange(
                  [ 0, endRange ],
                  `${licenseHeader}${separator}`
                );
              }
            }
          });
        }
      }
    };
  }
};


// helpers /////////////////////

/**
 * Creates a version of the LINEBREAK_MATCHER regex with the global flag.
 * Global regexes are mutable, so this needs to be a function instead of a constant.
 * @returns {RegExp} A global regular expression that matches line terminators
 */
function createGlobalLinebreakMatcher() {
  return new RegExp(LINEBREAK_MATCHER.source, 'gu');
}
