require 'gen'
require 'fileutils'
require 'open3'
require 'pathname'
require 'tmpdir'

module Gen
  class Generator
    def self.run(project_name)
      new(project_name).run
    end

    TEMPLATE_ROOT = File.expand_path('gen/template', Gen::ROOT)

    VALID_PROJECT_NAME = /\A[a-z][a-z0-9]*\z/
    private_constant :VALID_PROJECT_NAME

    # false  -> delete file
    # string -> rename file before applying template substitutions
    VENDOR_TRANSLATIONS = {
      'Gemfile' => false,
      'exe/__app__-gems' => false,
      'exe/__app__-vendor' => 'exe/__app__',
      'dev-gems.yml' => false,
      'dev-vendor.yml' => 'dev.yml',
    }.freeze
    private_constant :VENDOR_TRANSLATIONS

    BUNDLER_TRANSLATIONS = {
      'bin/update-deps' => false,
      'exe/__app__-gems' => 'exe/__app__',
      'exe/__app__-vendor' => false,
      'dev-gems.yml' => 'dev.yml',
      'dev-vendor.yml' => false,
    }.freeze
    private_constant :BUNDLER_TRANSLATIONS

    def initialize(project_name)
      raise(
        CLI::Kit::Abort,
        "project name must match {{bold:#{VALID_PROJECT_NAME}}} (but can be changed later)"
      ) unless project_name =~ VALID_PROJECT_NAME
      @project_name = project_name
      @title_case_project_name = @project_name.sub(/^./, &:upcase)
    end

    def run
      vendor = ask_vendor?
      create_project_dir
      if vendor
        copy_files(translations: VENDOR_TRANSLATIONS)
        update_deps
      else
        copy_files(translations: BUNDLER_TRANSLATIONS)
      end
    end

    private

    def ask_vendor?
      return 'vendor' if ENV['DEPS'] == 'vendor'
      return 'bundler' if ENV['DEPS'] == 'bundler'

      vendor = nil
      CLI::UI::Frame.open('Configuration') do
        q = 'How would you like the application to consume {{command:cli-kit}} and {{command:cli-ui}}?'
        vendor = CLI::UI::Prompt.ask(q) do |c|
          c.option('Vendor  {{italic:(faster execution, more difficult to update deps)}}') { 'vendor' }
          c.option('Bundler {{italic:(slower execution, easier dep management)}}') { 'bundler' }
        end
      end
      vendor == 'vendor'
    end

    def create_project_dir
      info(create: '')
      FileUtils.mkdir(@project_name)
    rescue Errno::EEXIST
      error("directory already exists: #{@project_name}")
    end

    def copy_files(translations:)
      each_template_file do |source_name|
        target_name = translations.fetch(source_name, source_name)
        next if target_name == false
        target_name = apply_template_variables(target_name)

        source = File.join(TEMPLATE_ROOT, source_name)
        target = File.join(@project_name, target_name)

        info(create: target_name)

        if Dir.exist?(source)
          FileUtils.mkdir(target)
        else
          content = apply_template_variables(File.read(source))
          File.write(target, content)
        end
        File.chmod(File.stat(source).mode, target)
      end
    end

    def update_deps
      Dir.mktmpdir do |tmp|
        clone(tmp, 'cli-ui')
        clone(tmp, 'cli-kit')
        info(run: 'bin/update-deps')
        Dir.chdir(@project_name) do
          system({ 'SOURCE_ROOT' => tmp }, 'bin/update-deps')
        end
      end
    end

    def clone(dir, repo)
      info(clone: repo)
      out, stat = Open3.capture2e('git', '-C', dir, 'clone', "https://github.com/shopify/#{repo}")
      unless stat.success?
        STDERR.puts(out)
        error("git clone failed")
      end
    end

    def each_template_file
      return enum_for(:each_template_file) unless block_given?

      root = Pathname.new(TEMPLATE_ROOT)
      Dir.glob("#{TEMPLATE_ROOT}/**/*").each do |f|
        el = Pathname.new(f)
        yield(el.relative_path_from(root).to_s)
      end
    end

    def apply_template_variables(s)
      s
        .gsub(/__app__/, @project_name)
        .gsub(/__App__/, @title_case_project_name)
        .gsub(/__cli-kit-version__/, cli_kit_version)
        .gsub(/__cli-ui-version__/, cli_ui_version)
    end

    def cli_kit_version
      require 'cli/kit/version'
      CLI::Kit::VERSION.to_s
    end

    def cli_ui_version
      require 'cli/ui/version'
      CLI::UI::VERSION.to_s
    end

    def info(create: nil, clone: nil, run: nil)
      if clone
        puts(CLI::UI.fmt("\t{{bold:{{yellow:clone}}\t#{clone}}}"))
      elsif create
        puts(CLI::UI.fmt("\t{{bold:{{blue:create}}\t#{create}}}"))
      elsif run
        puts(CLI::UI.fmt("\t{{bold:{{green:run}}\t#{run}}}"))
      end
    end

    def error(msg)
      raise(CLI::Kit::Abort, msg)
    end
  end
end