// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Helper tool that is built and run during a build to pull strings from the
// GRD files and generate a localized string files needed for iOS app bundles.
// Arguments:
//   -p dir_to_data_pak
//   -o output_dir
//   -c config_file
//   -I header_root_dir
//   and a list of locales.
//
// Example:
// generate_localizable_strings \
//   -p "${SHARED_INTERMEDIATE_DIR}/repack_ios/repack" \
//   -o "${INTERMEDIATE_DIR}/app_infoplist_strings" \
//   -c "<(DEPTH)/ios/chrome/localizable_strings_config.plist" \
//   -I "${SHARED_INTERMEDIATE_DIR}" \
//   ar ca cs da de el en-GB en-US es fi fr he hr hu id it ja ko ms nb nl pl \
//   pt pt-PT ro ru sk sv th tr uk vi zh-CN zh-TW

#import <Foundation/Foundation.h>

#import <iostream>
#import <map>
#import <set>
#import <string>
#import <string_view>
#import <utility>
#import <vector>

#import "base/apple/foundation_util.h"
#import "base/containers/span.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/tools/strings/grit_header_parsing.h"
#import "ui/base/resource/data_pack.h"
#import "ui/base/resource/resource_handle.h"
#import "ui/base/resource/resource_scale_factor.h"

namespace {

// Load the packed resource data pack for |locale| from |packed_data_pack_dir|.
// If loading fails, null is returned.
std::unique_ptr<ui::DataPack> LoadResourceDataPack(
    NSString* packed_data_pack_dir,
    NSString* locale_name) {
  std::unique_ptr<ui::DataPack> resource_data_pack;
  NSString* resource_path =
      [NSString stringWithFormat:@"%@/%@.lproj/locale.pak",
                                 packed_data_pack_dir, locale_name];

  if (!resource_path) {
    return resource_data_pack;
  }

  // FilePath may contain components that references parent directory
  // (".."). DataPack disallows paths with ".." for security reasons.
  base::FilePath resources_pak_path([resource_path fileSystemRepresentation]);
  resources_pak_path = base::MakeAbsoluteFilePath(resources_pak_path);
  if (!base::PathExists(resources_pak_path)) {
    return resource_data_pack;
  }

  resource_data_pack.reset(new ui::DataPack(ui::k100Percent));
  if (!resource_data_pack->LoadFromPath(resources_pak_path)) {
    resource_data_pack.reset();
  }

  return resource_data_pack;
}

// Create a |NSString| from the string with |resource_id| from |data_pack|.
// Return nil if none is found.
NSString* GetStringFromDataPack(const ui::DataPack& data_pack,
                                uint16_t resource_id) {
  std::optional<std::string_view> data = data_pack.GetStringView(resource_id);
  if (!data.has_value()) {
    return nil;
  }

  // Data pack encodes strings as either UTF8 or UTF16.
  if (data_pack.GetTextEncodingType() == ui::DataPack::UTF8) {
    return [[NSString alloc] initWithBytes:data->data()
                                    length:data->length()
                                  encoding:NSUTF8StringEncoding];
  } else if (data_pack.GetTextEncodingType() == ui::DataPack::UTF16) {
    return [[NSString alloc] initWithBytes:data->data()
                                    length:data->length()
                                  encoding:NSUTF16LittleEndianStringEncoding];
  }
  return nil;
}

// Generates a NSDictionary mapping string IDs to localized strings. The
// dictionary can be written as a Property List (only contains types that
// are valid in Propery Lists).
NSDictionary* GenerateLocalizableStringsDictionary(
    const ui::DataPack& data_pack,
    const char* locale,
    NSArray* resources,
    NSDictionary* resources_ids) {
  NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];
  for (id resource : resources) {
    NSString* resource_name = nil;
    NSString* resource_output_name = nil;
    if ([resource isKindOfClass:[NSString class]]) {
      resource_name = resource;
      resource_output_name = resource;
    } else if ([resource isKindOfClass:[NSDictionary class]]) {
      resource_name = [resource objectForKey:@"input"];
      resource_output_name = [resource objectForKey:@"output"];
      if (!resource_name || !resource_output_name) {
        std::cerr
            << "ERROR: resources must be given in <string> or <dict> format."
            << std::endl;
        return nil;
      }
    } else {
      std::cerr
          << "ERROR: resources must be given in <string> or <dict> format."
          << std::endl;
      return nil;
    }
    NSInteger resource_id =
        [[resources_ids objectForKey:resource_name] integerValue];
    NSString* string = GetStringFromDataPack(data_pack, resource_id);
    if (string) {
      [dictionary setObject:string forKey:resource_output_name];
    } else {
      std::cerr << "ERROR: fail to load string '"
                << base::SysNSStringToUTF8(resource_name) << "' for locale '"
                << locale << "'" << std::endl;
      return nil;
    }
  }

  return dictionary;
}

NSDictionary* LoadResourcesListFromHeaders(NSArray* header_list,
                                           NSString* root_header_dir) {
  if (![header_list count]) {
    std::cerr << "ERROR: No header file in the config." << std::endl;
    return nil;
  }

  std::vector<base::FilePath> headers;
  for (NSString* header in header_list) {
    headers.push_back(base::apple::NSStringToFilePath(
        [root_header_dir stringByAppendingPathComponent:header]));
  }

  std::optional<ResourceMap> resource_map =
      LoadResourcesFromGritHeaders(headers);
  if (!resource_map) {
    return nil;
  }

  NSMutableDictionary* resource_ids = [[NSMutableDictionary alloc] init];
  for (const auto& pair : *resource_map) {
    resource_ids[base::SysUTF8ToNSString(pair.first)] =
        [NSNumber numberWithInt:pair.second];
  }

  return [resource_ids copy];
}

// Save |dictionary| as a Property List file (in binary1 encoding)
// with |locale| to |output_dir|/|locale|.lproj/|output_filename|.
bool SavePropertyList(NSDictionary* dictionary,
                      NSString* locale,
                      NSString* output_dir,
                      NSString* output_filename) {
  // Compute the path to the output directory with locale.
  NSString* output_path = [output_dir
      stringByAppendingPathComponent:[NSString
                                         stringWithFormat:@"%@.lproj", locale]];

  // Prepare the directory.
  NSFileManager* file_manager = [NSFileManager defaultManager];
  if (![file_manager fileExistsAtPath:output_path] &&
      ![file_manager createDirectoryAtPath:output_path
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:nil]) {
    std::cerr << "ERROR: '" << base::SysNSStringToUTF8(output_path)
              << "' didn't exist or failed to create it." << std::endl;
    return false;
  }

  // Convert to property list in binary format.
  NSError* error = nil;
  NSData* data = [NSPropertyListSerialization
      dataWithPropertyList:dictionary
                    format:NSPropertyListBinaryFormat_v1_0
                   options:0
                     error:&error];
  if (!data) {
    std::cerr << "ERROR: conversion to property list failed: "
              << base::SysNSStringToUTF8([error localizedDescription])
              << std::endl;
    return false;
  }

  // Save the strings to the disk.
  output_path = [output_path stringByAppendingPathComponent:output_filename];
  if (![data writeToFile:output_path atomically:YES]) {
    std::cerr << "ERROR: Failed to write out '"
              << base::SysNSStringToUTF8(output_filename) << "'" << std::endl;
    return false;
  }

  return true;
}

int real_main(int argc, char** argv) {
  NSString* output_dir = nil;
  NSString* data_pack_dir = nil;
  NSString* root_header_dir = nil;
  NSString* config_file = nil;

  int ch;
  while ((ch = getopt(argc, argv, "c:I:p:o:h")) != -1) {
    switch (ch) {
      case 'c':
        config_file = base::SysUTF8ToNSString(optarg);
        break;
      case 'I':
        root_header_dir = base::SysUTF8ToNSString(optarg);
        break;
      case 'p':
        data_pack_dir = base::SysUTF8ToNSString(optarg);
        break;
      case 'o':
        output_dir = base::SysUTF8ToNSString(optarg);
        break;
      case 'h':
        std::cout << "usage: generate_localizable_strings  -p data_pack_dir -o "
                     "output_dir -c config_file -I input_root locale "
                     "[locale...]\n\nGenerate localized string files specified "
                     "in |config_file|\nfor all specified locales in "
                     "output_dir from packed data\npacks in data_pack_dir."
                  << std::endl;
        exit(0);
      default:
        std::cerr << "ERROR: bad command line arg: " << ch << std::endl;
        exit(1);
    }
  }

  if (!config_file) {
    std::cerr << "ERROR: missing config file." << std::endl;
    exit(1);
  }

  if (!root_header_dir) {
    std::cerr << "ERROR: missing root header dir." << std::endl;
    exit(1);
  }

  if (!output_dir) {
    std::cerr << "ERROR: missing output directory." << std::endl;
    exit(1);
  }

  if (!data_pack_dir) {
    std::cerr << "ERROR: missing data pack directory." << std::endl;
    exit(1);
  }

  if (optind == argc) {
    std::cerr << "ERROR: missing locale list." << std::endl;
    exit(1);
  }

  NSDictionary* config =
      [NSDictionary dictionaryWithContentsOfFile:config_file];

  NSDictionary* resources_ids = LoadResourcesListFromHeaders(
      [config objectForKey:@"headers"], root_header_dir);

  if (!resources_ids) {
    exit(1);
  }

  // SAFETY: libc ensure that main(...) is called with a buffer argv that
  // contains at least argc elements.
  const base::span<char*> args =
      UNSAFE_BUFFERS(base::span(argv, static_cast<size_t>(argc)))
          .subspan(static_cast<size_t>(optind));

  NSMutableArray* locales = [NSMutableArray arrayWithCapacity:args.size()];
  for (const char* arg : args) {
    // In order to find the locale at runtime, it needs to use '_' instead of
    // '-' (http://crbug.com/20441).  Also, 'en-US' should be represented
    // simply as 'en' (http://crbug.com/19165, http://crbug.com/25578).
    NSString* locale = base::SysUTF8ToNSString(arg);
    if ([locale isEqualToString:@"en-US"]) {
      locale = @"en";
    } else {
      locale = [locale stringByReplacingOccurrencesOfString:@"-"
                                                 withString:@"_"];
    }
    [locales addObject:locale];
  }

  NSArray* outputs = [config objectForKey:@"outputs"];

  if (![outputs count]) {
    std::cerr << "ERROR: No output in config file." << std::endl;
    exit(1);
  }

  for (NSString* locale in locales) {
    std::unique_ptr<ui::DataPack> data_pack =
        LoadResourceDataPack(data_pack_dir, locale);
    if (!data_pack) {
      std::cerr << "ERROR: Failed to load branded pak for language:"
                << base::SysNSStringToUTF8(locale) << std::endl;
      exit(1);
    }

    for (NSDictionary* output : [config objectForKey:@"outputs"]) {
      NSString* output_name = [output objectForKey:@"name"];
      if (!output_name) {
        std::cerr << "ERROR: Output without name." << std::endl;
        exit(1);
      }
      NSArray* output_strings = [output objectForKey:@"strings"];
      if (![output_strings count]) {
        std::cerr << "ERROR: Output without strings: "
                  << base::SysNSStringToUTF8(output_name) << std::endl;
        exit(1);
      }

      NSDictionary* dictionary = GenerateLocalizableStringsDictionary(
          *data_pack, base::SysNSStringToUTF8(locale).c_str(), output_strings,
          resources_ids);
      if (dictionary) {
        SavePropertyList(dictionary, locale, output_dir, output_name);
      } else {
        std::cerr << "ERROR: Unable to create: "
                  << base::SysNSStringToUTF8(output_name) << std::endl;
        exit(1);
      }
    }
  }
  return 0;
}

}  // namespace

int main(int argc, char** argv) {
  @autoreleasepool {
    return real_main(argc, argv);
  }
}
