// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// 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 the InfoPlist.strings files needed for
// Mac OS X app bundles.

#import <Foundation/Foundation.h>

#include <stdio.h>
#include <unistd.h>

#include "base/file_path.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/memory/scoped_ptr.h"
#include "base/string_piece.h"
#include "base/string_util.h"
#include "grit/chromium_strings.h"
#include "ui/base/resource/data_pack.h"

namespace {

NSString* ApplicationVersionString(const char* version_file_path) {
  NSError* error = nil;
  NSString* path_string = [NSString stringWithUTF8String:version_file_path];
  NSString* version_file =
      [NSString stringWithContentsOfFile:path_string
                                encoding:NSUTF8StringEncoding
                                   error:&error];
  if (!version_file || error) {
    fprintf(stderr, "Failed to load version file: %s\n",
            [[error description] UTF8String]);
    return nil;
  }

  int major = 0, minor = 0, build = 0, patch = 0;
  NSScanner* scanner = [NSScanner scannerWithString:version_file];
  if ([scanner scanString:@"MAJOR=" intoString:nil] &&
      [scanner scanInt:&major] &&
      [scanner scanString:@"MINOR=" intoString:nil] &&
      [scanner scanInt:&minor] &&
      [scanner scanString:@"BUILD=" intoString:nil] &&
      [scanner scanInt:&build] &&
      [scanner scanString:@"PATCH=" intoString:nil] &&
      [scanner scanInt:&patch]) {
    return [NSString stringWithFormat:@"%d.%d.%d.%d",
            major, minor, build, patch];
  }
  fprintf(stderr, "Failed to parse version file\n");
  return nil;
}

ui::DataPack* LoadResourceDataPack(const char* dir_path,
                                   const char* branding_strings_name,
                                   const char* locale_name) {
  ui::DataPack* resource_pack = NULL;

  NSString* resource_path = [NSString stringWithFormat:@"%s/%s_%s.pak",
                             dir_path, branding_strings_name, locale_name];
  if (resource_path) {
    FilePath resources_pak_path([resource_path fileSystemRepresentation]);
    resource_pack = new ui::DataPack;
    bool success = resource_pack->Load(resources_pak_path);
    if (!success) {
      delete resource_pack;
      resource_pack = NULL;
    }
  }

  return resource_pack;
}

NSString* LoadStringFromDataPack(ui::DataPack* data_pack,
                                 const char* data_pack_lang,
                                 uint32_t resource_id,
                                 const char* resource_id_str) {
  NSString* result = nil;
  base::StringPiece data;
  if (data_pack->GetStringPiece(resource_id, &data)) {
    // Data pack encodes strings as either UTF8 or UTF16.
    if (data_pack->GetTextEncodingType() == ui::DataPack::UTF8) {
      result =
          [[[NSString alloc] initWithBytes:data.data()
                                    length:data.length()
                                  encoding:NSUTF8StringEncoding]
           autorelease];
    } else if (data_pack->GetTextEncodingType() == ui::DataPack::UTF16) {
      result =
          [[[NSString alloc] initWithBytes:data.data()
                                    length:data.length()
                                  encoding:NSUTF16LittleEndianStringEncoding]
           autorelease];
    } else {
      fprintf(stderr, "ERROR: requested string %s from binary data pack\n",
              resource_id_str);
      exit(1);
    }
  }
  if (!result) {
    fprintf(stderr, "ERROR: failed to load string %s for lang %s\n",
            resource_id_str, data_pack_lang);
    exit(1);
  }
  return result;
}

// Escape quotes, newlines, etc so there are no errors when the strings file
// is parsed.
NSString* EscapeForStringsFileValue(NSString* str) {
  NSMutableString* worker = [NSMutableString stringWithString:str];

  // Since this is a build tool, we don't really worry about making this
  // the most efficient code.

  // Backslash first since we need to do it before we put in all the others
  [worker replaceOccurrencesOfString:@"\\"
                          withString:@"\\\\"
                             options:NSLiteralSearch
                               range:NSMakeRange(0, [worker length])];
  // Now the rest of them.
  [worker replaceOccurrencesOfString:@"\n"
                          withString:@"\\n"
                             options:NSLiteralSearch
                               range:NSMakeRange(0, [worker length])];
  [worker replaceOccurrencesOfString:@"\r"
                          withString:@"\\r"
                             options:NSLiteralSearch
                               range:NSMakeRange(0, [worker length])];
  [worker replaceOccurrencesOfString:@"\t"
                          withString:@"\\t"
                             options:NSLiteralSearch
                               range:NSMakeRange(0, [worker length])];
  [worker replaceOccurrencesOfString:@"\""
                          withString:@"\\\""
                             options:NSLiteralSearch
                               range:NSMakeRange(0, [worker length])];

  return [[worker copy] autorelease];
}

// The valid types for the -t arg
const char* kAppType_Main = "main";  // Main app
const char* kAppType_Helper = "helper";  // Helper app

}  // namespace

int main(int argc, char* const argv[]) {
  base::mac::ScopedNSAutoreleasePool autorelease_pool;

  const char* version_file_path = NULL;
  const char* grit_output_dir = NULL;
  const char* branding_strings_name = NULL;
  const char* output_dir = NULL;
  const char* app_type = kAppType_Main;

  // Process the args
  int ch;
  while ((ch = getopt(argc, argv, "t:v:g:b:o:")) != -1) {
    switch (ch) {
      case 't':
        app_type = optarg;
        break;
      case 'v':
        version_file_path = optarg;
        break;
      case 'g':
        grit_output_dir = optarg;
        break;
      case 'b':
        branding_strings_name = optarg;
        break;
      case 'o':
        output_dir = optarg;
        break;
      default:
        fprintf(stderr, "ERROR: bad command line arg\n");
        exit(1);
        break;
    }
  }
  argc -= optind;
  argv += optind;

#define CHECK_ARG(a, b) \
  do { \
    if ((a)) { \
      fprintf(stderr, "ERROR: " b "\n"); \
      exit(1); \
    } \
  } while (false)

  // Check our args
  CHECK_ARG(!version_file_path, "Missing VERSION file path");
  CHECK_ARG(!grit_output_dir, "Missing grit output dir path");
  CHECK_ARG(!output_dir, "Missing path to write InfoPlist.strings files");
  CHECK_ARG(!branding_strings_name, "Missing branding strings file name");
  CHECK_ARG(argc == 0, "Missing language list");
  CHECK_ARG((strcmp(app_type, kAppType_Main) != 0 &&
             strcmp(app_type, kAppType_Helper) != 0),
            "Unknown app type");

  char* const* lang_list = argv;
  int lang_list_count = argc;

  // Parse the version file and build our string
  NSString* version_string = ApplicationVersionString(version_file_path);
  if (!version_string) {
    fprintf(stderr, "ERROR: failed to get a version string");
    exit(1);
  }

  NSFileManager* fm = [NSFileManager defaultManager];

  for (int loop = 0; loop < lang_list_count; ++loop) {
    const char* cur_lang = lang_list[loop];

    // Open the branded string pak file
    scoped_ptr<ui::DataPack> branded_data_pack(
        LoadResourceDataPack(grit_output_dir,
                             branding_strings_name,
                             cur_lang));
    if (branded_data_pack.get() == NULL) {
      fprintf(stderr, "ERROR: Failed to load branded pak for language: %s\n",
              cur_lang);
      exit(1);
    }

    uint32_t name_id = IDS_PRODUCT_NAME;
    const char* name_id_str = "IDS_PRODUCT_NAME";
    uint32_t short_name_id = IDS_APP_MENU_PRODUCT_NAME;
    const char* short_name_id_str = "IDS_APP_MENU_PRODUCT_NAME";
    if (strcmp(app_type, kAppType_Helper) == 0) {
      name_id = IDS_HELPER_NAME;
      name_id_str = "IDS_HELPER_NAME";
      short_name_id = IDS_SHORT_HELPER_NAME;
      short_name_id_str = "IDS_SHORT_HELPER_NAME";
    }

    // Fetch the strings
    NSString* name =
          LoadStringFromDataPack(branded_data_pack.get(), cur_lang,
                                 name_id, name_id_str);
    NSString* short_name =
          LoadStringFromDataPack(branded_data_pack.get(), cur_lang,
                                 short_name_id, short_name_id_str);
    NSString* copyright =
        LoadStringFromDataPack(branded_data_pack.get(), cur_lang,
                               IDS_ABOUT_VERSION_COPYRIGHT,
                               "IDS_ABOUT_VERSION_COPYRIGHT");

    // For now, assume this is ok for all languages. If we need to, this could
    // be moved into generated_resources.grd and fetched.
    NSString *get_info = [NSString stringWithFormat:@"%@ %@, %@",
                          name, version_string, copyright];

    // Generate the InfoPlist.strings file contents
    NSString* strings_file_contents_string =
        [NSString stringWithFormat:
          @"CFBundleDisplayName = \"%@\";\n"
          @"CFBundleGetInfoString = \"%@\";\n"
          @"CFBundleName = \"%@\";\n"
          @"NSHumanReadableCopyright = \"%@\";\n",
          EscapeForStringsFileValue(name),
          EscapeForStringsFileValue(get_info),
          EscapeForStringsFileValue(short_name),
          EscapeForStringsFileValue(copyright)];

    // We set up Xcode projects expecting strings files to be UTF8, so make
    // sure we write the data in that form.  When Xcode copies them it will
    // put them final runtime encoding.
    NSData* strings_file_contents_utf8 =
        [strings_file_contents_string dataUsingEncoding:NSUTF8StringEncoding];

    if ([strings_file_contents_utf8 length] == 0) {
      fprintf(stderr, "ERROR: failed to get the utf8 encoding of the strings "
              "file for language: %s\n", cur_lang);
      exit(1);
    }

    // For Cocoa 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* cur_lang_ns = [NSString stringWithUTF8String:cur_lang];
    if ([cur_lang_ns isEqualToString:@"en-US"]) {
      cur_lang_ns = @"en";
    }
    cur_lang_ns = [cur_lang_ns stringByReplacingOccurrencesOfString:@"-"
                                                         withString:@"_"];
    // Make sure the lproj we write to exists
    NSString *lproj_name = [NSString stringWithFormat:@"%@.lproj", cur_lang_ns];
    NSString *output_path =
        [[NSString stringWithUTF8String:output_dir]
         stringByAppendingPathComponent:lproj_name];
    NSError* error = nil;
    if (![fm fileExistsAtPath:output_path] &&
        ![fm createDirectoryAtPath:output_path
        withIntermediateDirectories:YES
                        attributes:nil
                             error:&error]) {
      fprintf(stderr, "ERROR: '%s' didn't exist or we failed to create it\n",
              [output_path UTF8String]);
      exit(1);
    }

    // Write out the file
    output_path =
        [output_path stringByAppendingPathComponent:@"InfoPlist.strings"];
    if (![strings_file_contents_utf8 writeToFile:output_path
                                      atomically:YES]) {
      fprintf(stderr, "ERROR: Failed to write out '%s'\n",
              [output_path UTF8String]);
      exit(1);
    }
  }
  return 0;
}