// Copyright (c) 2011-2017 The OTS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "name.h"

#include <algorithm>
#include <cstring>
#include <cctype>

// name - Naming Table
// http://www.microsoft.com/typography/otspec/name.htm

namespace {

// We disallow characters outside the URI spec "unreserved characters"
// set; any chars outside this set will be replaced by underscore.
bool AllowedInPsName(char c) {
  return isalnum(c) || std::strchr("-._~", c);
}

bool SanitizePsNameAscii(std::string& name) {
  if (name.size() > 63)
    return false;

  for (unsigned i = 0; i < name.size(); ++i) {
    if (!AllowedInPsName(name[i])) {
      name[i] = '_';
    }
  }
  return true;
}

bool SanitizePsNameUtf16Be(std::string& name) {
  if ((name.size() & 1) != 0)
    return false;
  if (name.size() > 2 * 63)
    return false;

  for (unsigned i = 0; i < name.size(); i += 2) {
    if (name[i] != 0) {
      // non-Latin1 char in psname? reject it altogether
      return false;
    }
    if (!AllowedInPsName(name[i+1])) {
      name[i] = '_';
    }
  }
  return true;
}

void AssignToUtf16BeFromAscii(std::string* target,
                              const std::string& source) {
  target->resize(source.size() * 2);
  for (unsigned i = 0, j = 0; i < source.size(); i++) {
    (*target)[j++] = '\0';
    (*target)[j++] = source[i];
  }
}

}  // namespace


namespace ots {

bool OpenTypeNAME::Parse(const uint8_t* data, size_t length) {
  Buffer table(data, length);

  uint16_t format = 0;
  if (!table.ReadU16(&format) || format > 1) {
    return Error("Failed to read table format or bad format %d", format);
  }

  uint16_t count = 0;
  if (!table.ReadU16(&count)) {
    return Error("Failed to read name count");
  }

  uint16_t string_offset = 0;
  if (!table.ReadU16(&string_offset) || string_offset > length) {
    return Error("Failed to read or bad stringOffset");
  }
  const char* string_base = reinterpret_cast<const char*>(data) +
      string_offset;

  bool sort_required = false;

  // Read all the names, discarding any with invalid IDs,
  // and any where the offset/length would be outside the table.
  // A stricter alternative would be to reject the font if there
  // are invalid name records, but it's not clear that is necessary.
  for (unsigned i = 0; i < count; ++i) {
    NameRecord rec;
    uint16_t name_length, name_offset = 0;
    if (!table.ReadU16(&rec.platform_id) ||
        !table.ReadU16(&rec.encoding_id) ||
        !table.ReadU16(&rec.language_id) ||
        !table.ReadU16(&rec.name_id) ||
        !table.ReadU16(&name_length) ||
        !table.ReadU16(&name_offset)) {
      return Error("Failed to read name entry %d", i);
    }
    // check platform & encoding, discard names with unknown values
    switch (rec.platform_id) {
      case 0:  // Unicode
        if (rec.encoding_id > 6) {
          continue;
        }
        break;
      case 1:  // Macintosh
        if (rec.encoding_id > 32) {
          continue;
        }
        break;
      case 2:  // ISO
        if (rec.encoding_id > 2) {
          continue;
        }
        break;
      case 3:  // Windows: IDs 7 to 9 are "reserved"
        if (rec.encoding_id > 6 && rec.encoding_id != 10) {
          continue;
        }
        break;
      case 4:  // Custom (OTF Windows NT compatibility)
        if (rec.encoding_id > 255) {
          continue;
        }
        break;
      default:  // unknown platform
        continue;
    }

    const unsigned name_end = static_cast<unsigned>(string_offset) +
        name_offset + name_length;
    if (name_end > length) {
      continue;
    }
    rec.text.resize(name_length);
    rec.text.assign(string_base + name_offset, name_length);

    if (rec.name_id == 6) {
      // PostScript name: "sanitize" it by replacing any chars outside the
      // URI spec "unreserved" set by underscore, or reject the name entirely
      // (and use a fallback) if it looks really broken.
      if (rec.platform_id == 1) {
        if (!SanitizePsNameAscii(rec.text)) {
          continue;
        }
      } else if (rec.platform_id == 0 || rec.platform_id == 3) {
        if (!SanitizePsNameUtf16Be(rec.text)) {
          continue;
        }
      }
    }

    if (!this->names.empty() && !(this->names.back() < rec)) {
      Warning("name records are not sorted.");
      sort_required = true;
    }

    this->names.push_back(rec);
    this->name_ids.insert(rec.name_id);
  }

  if (format == 1) {
    // extended name table format with language tags
    uint16_t lang_tag_count;
    if (!table.ReadU16(&lang_tag_count)) {
      return Error("Failed to read langTagCount");
    }
    for (unsigned i = 0; i < lang_tag_count; ++i) {
      uint16_t tag_length = 0;
      uint16_t tag_offset = 0;
      if (!table.ReadU16(&tag_length) || !table.ReadU16(&tag_offset)) {
        return Error("Failed to read length or offset for langTagRecord %d", i);
      }
      const unsigned tag_end = static_cast<unsigned>(string_offset) +
          tag_offset + tag_length;
      if (tag_end > length) {
        return Error("bad end of tag %d > %ld for langTagRecord %d", tag_end, length, i);
      }
      // Lang tag is BCP 47 tag per the spec, the recommonded BCP 47 max tag
      // length is 35:
      // https://tools.ietf.org/html/bcp47#section-4.4.1
      // We are being too generous and allowing for 100 (multiplied by 2 since
      // this is UTF-16 string).
      if (tag_length > 100 * 2) {
        return Error("Too long language tag for LangTagRecord %d: %d", i, tag_length);
      }
      std::string tag(string_base + tag_offset, tag_length);
      this->lang_tags.push_back(tag);
    }
  }

  if (table.offset() > string_offset) {
    // the string storage apparently overlapped the name/tag records;
    // consider this font to be badly broken
    return Error("Bad table offset %ld > %d", table.offset(), string_offset);
  }

  // check existence of required name strings (synthesize if necessary)
  //  [0 - copyright - skip]
  //   1 - family
  //   2 - subfamily
  //  [3 - unique ID - skip]
  //   4 - full name
  //   5 - version
  //   6 - postscript name
  static const uint16_t kStdNameCount = 7;
  static const char* kStdNames[kStdNameCount] = {
    NULL,
    "OTS derived font",
    "Unspecified",
    NULL,
    "OTS derived font",
    "1.000",
    "OTS-derived-font"
  };

  // scan the names to check whether the required "standard" ones are present;
  // if not, we'll add our fixed versions here
  bool mac_name[kStdNameCount] = { 0 };
  bool win_name[kStdNameCount] = { 0 };
  for (const auto& name : this->names) {
    const uint16_t id = name.name_id;
    if (id >= kStdNameCount || kStdNames[id] == NULL) {
      continue;
    }
    if (name.platform_id == 1) {
      mac_name[id] = true;
      continue;
    }
    if (name.platform_id == 3) {
      win_name[id] = true;
      continue;
    }
  }

  for (uint16_t i = 0; i < kStdNameCount; ++i) {
    if (kStdNames[i] == NULL) {
      continue;
    }
    if (!mac_name[i] && !win_name[i]) {
      NameRecord mac_rec(1 /* platform_id */, 0 /* encoding_id */,
                         0 /* language_id */ , i /* name_id */);
      mac_rec.text.assign(kStdNames[i]);

      NameRecord win_rec(3 /* platform_id */, 1 /* encoding_id */,
                         1033 /* language_id */ , i /* name_id */);
      AssignToUtf16BeFromAscii(&win_rec.text, std::string(kStdNames[i]));

      this->names.push_back(mac_rec);
      this->names.push_back(win_rec);
      sort_required = true;
    }
  }

  if (sort_required) {
    std::sort(this->names.begin(), this->names.end());
  }

  return true;
}

bool OpenTypeNAME::Serialize(OTSStream* out) {
  uint16_t name_count = static_cast<uint16_t>(this->names.size());
  uint16_t lang_tag_count = static_cast<uint16_t>(this->lang_tags.size());
  uint16_t format = 0;
  size_t string_offset = 6 + name_count * 12;

  if (this->lang_tags.size() > 0) {
    // lang tags require a format-1 name table
    format = 1;
    string_offset += 2 + lang_tag_count * 4;
  }
  if (string_offset > 0xffff) {
    return Error("Bad stringOffset: %ld", string_offset);
  }
  if (!out->WriteU16(format) ||
      !out->WriteU16(name_count) ||
      !out->WriteU16(static_cast<uint16_t>(string_offset))) {
    return Error("Failed to write name header");
  }

  std::string string_data;
  for (const auto& rec : this->names) {
    if (string_data.size() + rec.text.size() >
            std::numeric_limits<uint16_t>::max() ||
        !out->WriteU16(rec.platform_id) ||
        !out->WriteU16(rec.encoding_id) ||
        !out->WriteU16(rec.language_id) ||
        !out->WriteU16(rec.name_id) ||
        !out->WriteU16(static_cast<uint16_t>(rec.text.size())) ||
        !out->WriteU16(static_cast<uint16_t>(string_data.size())) ) {
      return Error("Failed to write nameRecord");
    }
    string_data.append(rec.text);
  }

  if (format == 1) {
    if (!out->WriteU16(lang_tag_count)) {
      return Error("Failed to write langTagCount");
    }
    for (const auto& tag : this->lang_tags) {
      if (string_data.size() + tag.size() >
              std::numeric_limits<uint16_t>::max() ||
          !out->WriteU16(static_cast<uint16_t>(tag.size())) ||
          !out->WriteU16(static_cast<uint16_t>(string_data.size()))) {
        return Error("Failed to write langTagRecord");
      }
      string_data.append(tag);
    }
  }

  if (!out->Write(string_data.data(), string_data.size())) {
    return Error("Failed to write string data");
  }

  return true;
}

bool OpenTypeNAME::IsValidNameId(uint16_t nameID, bool addIfMissing) {
  if (addIfMissing && !this->name_ids.count(nameID)) {
    bool added_unicode = false;
    bool added_macintosh = false;
    bool added_windows = false;
    const size_t names_size = this->names.size();  // original size
    for (size_t i = 0; i < names_size; ++i) switch (names[i].platform_id) {
     case 0:
      if (!added_unicode) {
        // If there is an existing NameRecord with platform_id == 0 (Unicode),
        // then add a NameRecord for the the specified nameID with arguments
        // 0 (Unicode), 0 (v1.0), 0 (unspecified language).
        this->names.emplace_back(0, 0, 0, nameID);
        this->names.back().text = "NoName";
        added_unicode = true;
      }
      break;
     case 1:
      if (!added_macintosh) {
        // If there is an existing NameRecord with platform_id == 1 (Macintosh),
        // then add a NameRecord for the specified nameID with arguments
        // 1 (Macintosh), 0 (Roman), 0 (English).
        this->names.emplace_back(1, 0, 0, nameID);
        this->names.back().text = "NoName";
        added_macintosh = true;
      }
      break;
     case 3:
      if (!added_windows) {
        // If there is an existing NameRecord with platform_id == 3 (Windows),
        // then add a NameRecord for the specified nameID with arguments
        // 3 (Windows), 1 (UCS), 1033 (US English).
        this->names.emplace_back(3, 1, 1033, nameID);
        this->names.back().text = "NoName";
        added_windows = true;
      }
      break;
    }
    if (added_unicode || added_macintosh || added_windows) {
      std::sort(this->names.begin(), this->names.end());
      this->name_ids.insert(nameID);
    }
  }
  return this->name_ids.count(nameID);
}

// List of font names considered "tricky" (dependent on applying original TrueType instructions) by FreeType, see
// https://gitlab.freedesktop.org/freetype/freetype/-/blob/2d9fce53d4ce89f36075168282fcdd7289e082f9/src/truetype/ttobjs.c#L170-241
static const char* tricky_font_names[] = {
  "cpop",
  "DFGirl-W6-WIN-BF",
  "DFGothic-EB",
  "DFGyoSho-Lt",
  "DFHei",
  "DFHSGothic-W5",
  "DFHSMincho-W3",
  "DFHSMincho-W7",
  "DFKaiSho-SB",
  "DFKaiShu",
  "DFKai-SB",
  "DFMing",
  "DLC",
  "HuaTianKaiTi?",
  "HuaTianSongTi?",
  "Ming(for ISO10646)",
  "MingLiU",
  "MingMedium",
  "PMingLiU",
  "MingLi43"
};

bool OpenTypeNAME::IsTrickyFont() const {
  for (const auto& name : this->names) {
    const uint16_t id = name.name_id;
    if (id != 1) {
      continue;
    }
    for (const auto* p : tricky_font_names) {
      if (name.text.find(p) != std::string::npos) {
        return true;
      }
    }
  }
  return false;
}

}  // namespace
