/*** Generated by streamline 0.10.17 (callbacks) - DO NOT EDIT ***//**
 * Copyright (c) Microsoft.  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.
 */

var __ = require('underscore');
var util = require('util');
var utils = require('../../../util/utils');
var constants = require('./constants');
var moment = require('moment');

function ZoneFile(output) {
  this.output = output;
  this.defaultTtl = 3600;
}

__.extend(ZoneFile.prototype, {

  /**
   * Returns JSON object which represents standard zone file structure.
   */
  parse: function (zoneName, text) {
    text = this.removeComments(text);
    text = this.flatten(text);
    return this.parseRRs(zoneName, text);
  },

  /**
   * Generates standard zone file content from Azure DNS zone object.
   */
  generate: function (resourceGroupName, zoneName, recordSets) {
    var ttl = recordSets.recordSets[0].properties.ttl;

    var fileData = '';
    fileData = this.generateHeader(fileData, resourceGroupName, zoneName, ttl);
    fileData = this.generateRRs(fileData, recordSets);
    return fileData;
  },

  /**
   * Parse methods
   */
  flatten: function (text) {
    var bracketsPattern = /\([\s\S]*?\)/gim;
    var match = bracketsPattern.exec(text);
    while (match !== null) {
      match.replacement = match[0].replace(/\s+/gm, ' ').replace('(', '').replace(')', '').trim();
      var textAsCharsArray = text.split('');
      textAsCharsArray.splice(match.index, match[0].length, match.replacement); // replace multi-line chars with single-line instead
      text = textAsCharsArray.join('');
      bracketsPattern = /\([\s\S]*?\)/gim;
      match = bracketsPattern.exec(text);
    }

    return text;
  },

  removeComments: function (text) {
    return text.replace(/;[\s\S]*?$/gm, '');
  },

  parseRRs: function (zoneName, text) {
    var self = this;
    var res = {
      sets: []
    };

    zoneName = zoneName.toLowerCase();
    if (!utils.stringEndsWith(zoneName, '.', true)) zoneName += '.';

    var rrs = text.split('\n');
    for (var i in rrs) {
      var rr = rrs[i];
      if (!rr || !rr.trim()) {
        continue;
      }
      var recordSet;
      var prevRRname;

      if (utils.stringStartsWith(rr, '$', true)) {
        self.parseDirective(rr, res);
      } else {
        if (res.$origin === undefined) res.$origin = zoneName;
        var baseRR = this.parseRR(rr, res.$origin, zoneName, res.$ttl, prevRRname);
        if (baseRR.error) {
          this.output.warn(baseRR.error);
          prevRRname = baseRR.name;
          continue;
        }
        switch (baseRR.type) {
          case 'SOA':
            recordSet = self.parseSOA(baseRR, res.$origin);
            break;
          case 'NS':
            recordSet = self.parseNS(baseRR, res.$origin);
            break;
          case 'A':
            recordSet = self.parseA(baseRR);
            break;
          case 'AAAA':
            recordSet = self.parseAAAA(baseRR);
            break;
          case 'CNAME':
            recordSet = self.parseCNAME(baseRR, res.$origin);
            break;
          case 'TXT':
            recordSet = self.parseTXT(baseRR);
            break;
          case 'MX':
            recordSet = self.parseMX(baseRR, res.$origin);
            break;
          case '%PTR': // note: PTR is not supported yet on server end
            recordSet = self.parsePTR(baseRR, res.$origin);
            break;
          case 'SRV':
            recordSet = self.parseSRV(baseRR, res.$origin);
            break;
          default:
            this.output.warn(util.format('Record of unsupported type: %s', rr));
            continue;
        }
      }

      if (recordSet) {
        var index = utils.indexOfCaseIgnore(res.sets, {name: recordSet.name, type: recordSet.type});
        if (index === -1) {
          res.sets.push(recordSet); // create new RecordSet
        } else {
          res.sets[index].records.push(recordSet.records[0]); // Use existing Record set
          if (recordSet.ttl !== res.sets[index].ttl) {
            var minttl = recordSet.ttl < res.sets[index].ttl ? recordSet.ttl : res.sets[index].ttl;
            this.output.warn(util.format('The TTLs %s and %s for record set "%s" are conflicts, using lower TTL of %s',
              recordSet.ttl, res.sets[index].ttl, recordSet.name, minttl));

            res.sets[index].ttl = minttl;
          }
        }
        prevRRname = recordSet.name;
        recordSet = undefined;
      }
    }
    return res;
  },

  parse$ORIGIN: function (rr) {
    var self = this;
    var origin = rr.split(/\s+/g)[1];
    if (!utils.stringEndsWith(origin, '.', true)) {
      self.output.warn(util.format('The value "%s" of $ORIGIN directive doesn\'t end with a dot, dot added', origin));
      origin += '.';
    }
    return origin;
  },

  parse$TTL: function (rr) {
    var value = rr.split(/\s+/g)[1];
    var ttl = this.parseTimestamp(value);
    return ttl;
  },

  /**
   * Returns seconds number from 1m, 2h, 3d, 4m TTL time format
   */
  parseTimestamp: function (str) {
    str = str.toLowerCase();

    var seconds = 0;
    var chunk = '';
    var prevChar;

    for (var i = 0, currChar = ''; i < str.length; i++) {
      currChar = str.charAt(i);
      if (!isNaN(parseInt(currChar))) {
        chunk += currChar;
      } else {
        if (i === 0) return undefined;
        if (isNaN(parseInt(prevChar))) return undefined;
        switch (currChar) {
          case 's':
            seconds += chunk * 1;
            break;
          case 'm':
            seconds += chunk * 60;
            break;
          case 'h':
            seconds += chunk * 3600;
            break;
          case 'd':
            seconds += chunk * 86400;
            break;
          case 'w':
            seconds += chunk * 604800;
            break;
          default:
            return undefined;
        }
        chunk = '';
      }
      prevChar = currChar;
    }

    if (chunk.length > 0) seconds += parseInt(chunk);
    return seconds;
  },

  parseRR: function (rr, $origin, zoneName, $ttl, prevRRname) {
    var self = this;
    var res = {
      data: []
    };

    var validTypes = ['SOA', 'NS', 'A', 'AAAA', 'CNAME', 'TXT', 'MX', 'PTR', 'SRV'];
    var validClasses = ['IN', 'CS', 'CH', 'HS'];
    var rrTokens = rr.trim().split(/\s+/g);

    if (rrTokens.length < 2) {
      res.error = util.format('Invalid record format: %s', rr);
      return res;
    }

    var position = 0;
    while (rrTokens.length > 0) {
      var token = rrTokens.shift();
      if (position === 0) {
        res.name = token;
      } else if (validClasses.indexOf(token.toUpperCase()) !== -1 && (position <= 2)) {
        res.class = token.toUpperCase();
      } else if (validTypes.indexOf(token.toUpperCase()) !== -1 && (position <= 3)) {
        res.type = token.toUpperCase();
      } else if (self.parseTimestamp(token) && (position <= 1) && (res.type === undefined)) {
        res.ttl = self.parseTimestamp(token);
      } else {
        res.data.push(token);
      }
      position++;
    }

    if (res.data.length < 1) {
      res.error = util.format('Invalid record format: %s', rr);
      return res;
    }

    if (!res.type) {
      res.error = util.format('Record of unsupported type: %s', rr);
      return res;
    }

    if (!res.ttl) res.ttl = $ttl === undefined ? this.defaultTtl : $ttl;
    if (!res.name) res.name = prevRRname;

    var fqdnName = self.convertToFQDN(res.name, $origin);
    if (!utils.stringEndsWith(fqdnName, zoneName, true)) {
      res.error = util.format('The record set with fully-qualified name "%s" does not match the zone name "%s", skipped', fqdnName, zoneName);
      return res;
    }
    res.name = fqdnName;

    return res;
  },

  parseDirective: function (rr, res) {
    var self = this;
    var uRR = rr.toUpperCase();
    if (uRR.indexOf('$ORIGIN') === 0) {
      res.$origin = self.parse$ORIGIN(rr);
    } else if (uRR.indexOf('$TTL') === 0) {
      res.$ttl = self.parse$TTL(rr);
    } else {
      self.output.warn(util.format('Unrecognized directive: %s', rr));
    }
  },

  convertToFQDN: function (value, $origin) {
    if (value === '@') return $origin;
    if (utils.stringEndsWith(value, '.', true)) return value;
    if (!$origin) {
      this.output.warn('$ORIGIN directive is not defined');
      return value;
    } else {
      return value + '.' + $origin;
    }
  },

  parseSOA: function (rr, $origin) {
    var self = this;

    var soa = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        host: rr.data[0],
        email: self.convertToFQDN(rr.data[1], $origin),
        serialNumber: self.parseTimestamp(rr.data[2]),
        refreshTime: self.parseTimestamp(rr.data[3]),
        retryTime: self.parseTimestamp(rr.data[4]),
        expireTime: self.parseTimestamp(rr.data[5]),
        minimumTtl: self.parseTimestamp(rr.data[6])
      }]
    };
    return soa;
  },

  parseNS: function (rr, $origin) {
    var self = this;

    var ns = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        nsdname: self.convertToFQDN(rr.data[0], $origin)
      }]
    };
    return ns;
  },

  parseA: function (rr) {
    var a = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        ipv4Address: rr.data[0]
      }]
    };
    return a;
  },

  parseAAAA: function (rr) {
    var aaaa = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        ipv6Address: rr.data[0]
      }]
    };
    return aaaa;
  },

  parseCNAME: function (rr, $origin) {
    var self = this;
    var cname = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        cname: self.convertToFQDN(rr.data[0], $origin)
      }]
    };
    return cname;
  },

  parseMX: function (rr, $origin) {
    var self = this;
    var mx = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        preference: parseInt(rr.data[0]),
        exchange: self.convertToFQDN(rr.data[1], $origin)
      }]
    };
    return mx;
  },

  parseTXT: function (rr) {
    var value = '';
    var plainText = rr.data.join(' ');
    var matches = plainText.match(/[^"]+(?=(" ")|"$)/g);
    if (matches) {
      value = matches.join('');
    } else {
      value = plainText;
    }

    if (value.length > 255) {
      value = value.substr(0, 255);
      this.output.warn(util.format('TXT record "%s" value exceeds the maximum length of 255 chars, truncated', rr.name));
    }

    var txt = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        value: value.trim()
      }]
    };
    return txt;
  },

  parsePTR: function (rr, $origin) {
    var self = this;
    var ptr = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        ptrdname: self.convertToFQDN(rr.data[0], $origin)
      }]
    };
    return ptr;
  },

  parseSRV: function (rr, $origin) {
    var self = this;
    var srv = {
      name: rr.name,
      type: rr.type,
      ttl: rr.ttl,
      records: [{
        priority: parseInt(rr.data[0]),
        weight: parseInt(rr.data[1]),
        port: parseInt(rr.data[2]),
        target: self.convertToFQDN(rr.data[3], $origin)
      }]
    };
    return srv;
  },

  /**
   * Generate methods
   */
  generateHeader: function (fileData, resourceGroupName, zoneName, ttl) {
    fileData += '; Exported zone file from Azure DNS\r\n';
    fileData += util.format('; Resource Group Name: %s\r\n', resourceGroupName);
    fileData += util.format('; Zone name: %s\n', zoneName);
    fileData += util.format('; Date and time (UTC): %s\r\n\r\n', moment.utc());
    fileData += util.format('$TTL %s\r\n', ttl);
    fileData += util.format('$ORIGIN %s.\r\n\r\n', zoneName);
    return fileData;
  },

  generateRRs: function (fileData, sets) {
    var self = this;

    var index;
    var soaSet = __.find(sets.recordSets, function (rset, rindex) {
      if (rset.properties.soaRecord) {
        index = rindex;
        return true;
      }
    });

    if (typeof index !== 'undefined') {
      fileData += self.generateFromSet(soaSet, 'soaRecord', constants.dnsZone.SOA, soaSet.properties.ttl);
      sets.recordSets.splice(index, 1);
    }

    sets.recordSets.forEach(function (rset) {
      var ttl = rset.properties.ttl;

      fileData += self.generateFromSet(rset, 'aRecords', constants.dnsZone.A, ttl);
      fileData += self.generateFromSet(rset, 'aaaaRecords', constants.dnsZone.AAAA, ttl);
      fileData += self.generateFromSet(rset, 'txtRecords', constants.dnsZone.TXT, ttl);
      fileData += self.generateFromSet(rset, 'ptrRecords', constants.dnsZone.PTR, ttl);
      fileData += self.generateFromSet(rset, 'mxRecords', constants.dnsZone.MX, ttl);
      fileData += self.generateFromSet(rset, 'nsRecords', constants.dnsZone.NS, ttl);
      fileData += self.generateFromSet(rset, 'srvRecords', constants.dnsZone.SRV, ttl);
      fileData += self.generateFromSet(rset, 'cnameRecord', constants.dnsZone.CNAME, ttl);
    });
    return fileData;
  },

  generateFromSet: function (recordSet, propertyName, type, ttl) {
    var self = this;
    var fileData = '';

    var obj = recordSet.properties[propertyName];

    if (!__.isEmpty(obj)) {
      if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; i++) {
          var record = obj[i];
          var recordName;
          if (i === 0) {
            recordName = recordSet.name;
          } else {
            recordName = utils.setIndent(recordSet.name.length);
          }
          fileData += self.generateRR(recordName, type, ttl, record);
        }
        fileData += '\r\n';
      } else {
        fileData += self.generateRR(recordSet.name, type, ttl, obj);
        fileData += '\r\n';
      }
    }
    return fileData;
  },

  generateRR: function (name, type, ttl, record) {
    var self = this;
    var defClass = constants.dnsZone.recordClasses[0];

    var rr = util.format('%s %s %s %s ', name, ttl, defClass, type);
    switch (type) {
      case constants.dnsZone.SOA:
        var serialNumber = record.serialNumber || moment().format('YYYYMMDDss');
        if (!utils.stringEndsWith(record.host, '.', true)) record.host += '.';
        if (!utils.stringEndsWith(record.email, '.', true)) record.email += '.';
        rr += util.format('%s %s (\r\n\t\t\t\t%s\r\n\t\t\t\t%s\r\n\t\t\t\t%s\r\n\t\t\t\t%s\r\n\t\t\t\t%s\r\n\t\t\t\t)', record.host, record.email,
          serialNumber, record.refreshTime, record.retryTime, record.expireTime, record.minimumTtl);
        break;
      case constants.dnsZone.A:
        rr += record.ipv4Address;
        break;
      case constants.dnsZone.AAAA:
        rr += record.ipv6Address;
        break;
      case constants.dnsZone.CNAME:
        rr += record.cname;
        if (!utils.stringEndsWith(record.cname, '.', true)) rr += '.';
        break;
      case constants.dnsZone.MX:
        rr += util.format('%s %s', record.preference, record.exchange);
        if (!utils.stringEndsWith(record.exchange, '.', true)) rr += '.';
        break;
      case constants.dnsZone.NS:
        rr += record.nsdname;
        if (!utils.stringEndsWith(record.nsdname, '.', true)) rr += '.';
        break;
      case constants.dnsZone.SRV:
        rr += util.format('%s %s %s %s', record.priority, record.weight, record.port, record.target);
        if (!utils.stringEndsWith(record.target, '.', true)) rr += '.';
        break;
      case constants.dnsZone.TXT:
        rr += util.format('"%s"', record.value);
        break;
      case constants.dnsZone.PTR:
        rr += record.ptrdname;
        break;
      default:
        self.output.warn(util.format('Records of type "%s" are not supported, record with name "%s" not exported', type, name));
        return '';
    }

    rr += '\r\n';
    return rr;
  }

});

module.exports = ZoneFile;
