import { isEqual } from 'lodash-es';

import { t } from '../core/localizer';
import { osmAreaKeys, osmAreaKeysExceptions } from '../osm/tags';
import { utilObjectOmit } from '../util';
import { utilSafeClassName } from '../util/util';
import { locationManager } from '../core/LocationManager';


//
// `presetPreset` decorates a given `preset` Object
// with some extra methods for searching and matching geometry
//
export function presetPreset(presetID, preset, addable, allFields, allPresets) {
  allFields = allFields || {};
  allPresets = allPresets || {};
  let _this = Object.assign({}, preset); // shallow copy
  let _addable = addable || false;
  let _searchName;            // cache
  let _searchNameStripped;    // cache
  let _searchAliases;         // cache
  let _searchAliasesStripped; // cache

  const referenceRegex = /^\{(.*)\}$/;

  _this.id = presetID;

  _this.safeid = utilSafeClassName(presetID);  // for use in css classes, selectors, element ids

  _this.originalTerms = (_this.terms || []).join();

  _this.originalName = _this.name || '';

  _this.originalAliases = (_this.aliases || []).join('\n');

  _this.originalScore = _this.matchScore || 1;

  _this.originalReference = _this.reference || {};

  _this.originalFields = (_this.fields || []);

  _this.originalMoreFields = (_this.moreFields || []);

  _this.fields = loc => resolveFields('fields', loc);

  _this.moreFields = loc => resolveFields('moreFields', loc);

  _this.tags = _this.tags || {};

  _this.addTags = _this.addTags || _this.tags;

  _this.removeTags = _this.removeTags || _this.addTags;

  _this.geometry = (_this.geometry || []);

  _this.matchGeometry = (geom) => _this.geometry.indexOf(geom) >= 0;

  _this.matchAllGeometry = (geoms) => geoms.every(_this.matchGeometry);

  _this.matchScore = (entityTags) => {
    const tags = _this.tags;
    let seen = {};
    let score = 0;

    // match on tags
    for (let k in tags) {
      seen[k] = true;
      if (entityTags[k] === tags[k]) {
        score += _this.originalScore;
      } else if (tags[k] === '*' && k in entityTags) {
        score += _this.originalScore / 2;
      } else {
        return -1;
      }
    }

    // boost score for additional matches in addTags - #6802
    const addTags = _this.addTags;
    for (let k in addTags) {
      if (!seen[k] && entityTags[k] === addTags[k]) {
        score += _this.originalScore;
      }
    }

    if (_this.searchable === false) {
      score *= 0.999;
    }

    return score;
  };


  _this.t = (scope, options) => {
    const textID = `_tagging.presets.presets.${presetID}.${scope}`;
    return t(textID, options);
  };

  _this.t.append = (scope, options) => {
    const textID = `_tagging.presets.presets.${presetID}.${scope}`;
    return t.append(textID, options);
  };

  function resolveReference(which) {
    const match = (_this[which] || '').match(referenceRegex);
    if (match) {
      const preset = allPresets[match[1]];
      if (preset) {
        return preset;
      }
      console.error(`Unable to resolve referenced preset: ${match[1]}`);  // eslint-disable-line no-console
    }
    return _this;
  }

  _this.name = () => {
    return resolveReference('originalName')
      .t('name', { 'default': _this.originalName || presetID });
  };

  _this.nameLabel = () => {
    return resolveReference('originalName')
      .t.append('name', { 'default': _this.originalName || presetID });
  };

  _this.subtitle = () => {
      if (_this.suggestion) {
        let path = presetID.split('/');
        path.pop();  // remove brand name
        return t('_tagging.presets.presets.' + path.join('/') + '.name');
      }
      return null;
  };

  _this.subtitleLabel = () => {
      if (_this.suggestion) {
        let path = presetID.split('/');
        path.pop();  // remove brand name
        return t.append('_tagging.presets.presets.' + path.join('/') + '.name');
      }
      return null;
  };

  _this.aliases = () => {
    return resolveReference('originalName')
      .t('aliases', { 'default': _this.originalAliases })
      .trim()
      .split(/\s*[\r\n]+\s*/)
      .filter(Boolean);
  };

  _this.terms = () => {
    return resolveReference('originalName')
      .t('terms', { 'default': _this.originalTerms })
      .toLowerCase().trim().split(/\s*,+\s*/);
  };

  _this.searchName = () => {
    if (!_searchName) {
      _searchName = (_this.suggestion ? _this.originalName : _this.name()).toLowerCase();
    }
    return _searchName;
  };

  _this.searchNameStripped = () => {
    if (!_searchNameStripped) {
      _searchNameStripped = stripDiacritics(_this.searchName());
    }
    return _searchNameStripped;
  };

  _this.searchAliases = () => {
    if (!_searchAliases) {
      _searchAliases = _this.aliases().map(alias => alias.toLowerCase());
    }
    return _searchAliases;
  };

  _this.searchAliasesStripped = () => {
    if (!_searchAliasesStripped) {
      _searchAliasesStripped = _this.searchAliases();
      _searchAliasesStripped = _searchAliasesStripped.map(stripDiacritics);
    }
    return _searchAliasesStripped;
  };

  _this.isFallback = () => {
    const tagCount = Object.keys(_this.tags).length;
    return tagCount === 0 || (tagCount === 1 && _this.tags.hasOwnProperty('area'));
  };


  _this.addable = function(val) {
    if (!arguments.length) return _addable;
    _addable = val;
    return _this;
  };


  _this.reference = () => {
    // Lookup documentation on Wikidata...
    const qid = (
      _this.tags.wikidata ||
      _this.tags['flag:wikidata'] ||
      _this.tags['brand:wikidata'] ||
      _this.tags['network:wikidata'] ||
      _this.tags['operator:wikidata']
    );
    if (qid) {
      return { qid: qid };
    }

    // Lookup documentation on OSM Wikibase...
    let key = _this.originalReference.key || Object.keys(utilObjectOmit(_this.tags, 'name'))[0];
    let value = _this.originalReference.value || _this.tags[key];

    if (value === '*') {
      return { key: key };
    } else {
      return { key: key, value: value };
    }
  };


  _this.unsetTags = (tags, geometry, ignoringKeys, skipFieldDefaults, loc) => {
    // allow manually keeping some tags
    let removeTags = ignoringKeys ? utilObjectOmit(_this.removeTags, ignoringKeys) : _this.removeTags;
    tags = utilObjectOmit(tags, Object.keys(removeTags));

    if (geometry && !skipFieldDefaults) {
      _this.fields(loc).forEach(field => {
        if (field.matchGeometry(geometry) && field.key &&
            field.default === tags[field.key] &&
            (!ignoringKeys || ignoringKeys.indexOf(field.key) === -1)) {
          delete tags[field.key];
        }
      });
    }

    delete tags.area;
    return tags;
  };


  _this.setTags = (tags, geometry, skipFieldDefaults, loc) => {
    const addTags = _this.addTags;
    tags = Object.assign({}, tags);   // shallow copy

    for (let k in addTags) {
      if (addTags[k] === '*') {
        // if this tag is ancillary, don't override an existing value since any value is okay
        if (_this.tags[k] || !tags[k]) {
          tags[k] = 'yes';
        }
      } else {
        tags[k] = addTags[k];
      }
    }

    // Add area=yes if necessary.
    // This is necessary if the geometry is already an area (e.g. user drew an area) AND any of:
    // 1. chosen preset could be either an area or a line (`barrier=city_wall`)
    // 2. chosen preset doesn't have a key in osmAreaKeys (`railway=station`),
    //    and is not an "exceptional area" tag (e.g. `waterway=dam`)
    if (!addTags.hasOwnProperty('area')) {
      delete tags.area;
      if (geometry === 'area') {
        let needsAreaTag = true;
        for (let k in addTags) {
          if (_this.geometry.indexOf('line') === -1 && k in osmAreaKeys
              || k in osmAreaKeysExceptions && addTags[k] in osmAreaKeysExceptions[k]) {
            needsAreaTag = false;
            break;
          }
        }
        if (needsAreaTag) {
          tags.area = 'yes';
        }
      }
    }

    if (geometry && !skipFieldDefaults) {
      _this.fields(loc).forEach(field => {
        if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) {
          tags[field.key] = field.default;
        }
      });
    }

    return tags;
  };


  // For a preset without fields, use the fields of the parent preset.
  // Replace {preset} placeholders with the fields of the specified presets.
  function resolveFields(which, loc) {
    const fieldIDs = (which === 'fields' ? _this.originalFields : _this.originalMoreFields);
    let resolved = [];

    fieldIDs.forEach(fieldID => {
      const match = fieldID.match(referenceRegex);
      if (match !== null) {    // a presetID wrapped in braces {}
        resolved = resolved.concat(inheritFields(allPresets[match[1]], which, loc));
      } else if (allFields[fieldID]) {    // a normal fieldID
        resolved.push(allFields[fieldID]);
      } else {
        console.log(`Cannot resolve "${fieldID}" found in ${_this.id}.${which}`);  // eslint-disable-line no-console
      }
    });

    // no fields resolved, so use the parent's if possible
    if (!resolved.length) {
      const endIndex = _this.id.lastIndexOf('/');
      const parentID = endIndex && _this.id.substring(0, endIndex);
      if (parentID) {
        let parent = allPresets[parentID];
        if (loc) {
          const validHere = locationManager.locationSetsAt(loc);
          if (parent?.locationSetID && !validHere[parent.locationSetID]) {
            // this is a preset for which a regional variant of the main preset exists
            const candidateIDs = Object.keys(allPresets).filter(k => k.startsWith(parentID));
            parent = allPresets[candidateIDs.find(candidateID => {
              const candidate = allPresets[candidateID];
              return validHere[candidate.locationSetID] && isEqual(candidate.tags, parent.tags);
            })];
          }
        }
        resolved = inheritFields(parent, which, loc);
      }
    }

    if (loc) {
      const validHere = locationManager.locationSetsAt(loc);
      resolved = resolved.filter(field => !field.locationSetID || validHere[field.locationSetID]);
    }

    return resolved;


    // returns an array of fields to inherit from the given presetID, if found
    function inheritFields(parent, which, loc) {
      if (!parent) return [];

      if (which === 'fields') {
        return parent.fields(loc).filter(shouldInherit);
      } else if (which === 'moreFields') {
        return parent.moreFields(loc).filter(shouldInherit);
      } else {
        return [];
      }
    }


    // Skip `fields` for the keys which define the preset.
    // These are usually `typeCombo` fields like `shop=*`
    function shouldInherit(f) {
      if (f.key && _this.tags[f.key] !== undefined &&
        // inherit anyway if multiple values are allowed or just a checkbox
        f.type !== 'multiCombo' && f.type !== 'semiCombo' && f.type !== 'manyCombo' && f.type !== 'check'
      ) return false;
      if (f.key && (_this.originalFields.some(originalField => f.key === allFields[originalField]?.key)
             || _this.originalMoreFields.some(originalField => f.key === allFields[originalField]?.key))) {
        // current preset already has a field for this field
        return false;
      }

      return true;
    }
  }


  function stripDiacritics(s) {
    // split combined diacritical characters into their parts
    if (s.normalize) s = s.normalize('NFD');
    // remove diacritics
    s = s.replace(/[\u0300-\u036f]/g, '');
    return s;
  }

  return _this;
}
