require_relative '../lib'

class VueCreate
  def initialize
    @input = InputLoop.new
    @pwd = FileUtils.pwd
    @root = ::Rails.root
    @pack = @root.join('package.json')
    @src_dir = Pathname.new(__FILE__).dirname.join('..', '..', 'source')
  end

  def self.run!
    new.start
  end

  def start
    check_node!
    FileUtils.chdir(@root)
    @pending_install = false
    @pending_fixes = []
    begin
      package_manager?
      install_vue_cli
      run_vue_create?
      install_dev_deps
      @pm.install if @pending_install
      delete_vue_sample?
      copy_demo?
      copy_config
      generate_vue_yml
      ensure_entry_point!
      fix_jest_config!
      eslint_fix!
      puts 'vue:create finished!'
    ensure
      FileUtils.chdir(@pwd)
    end
  end

  private

  def check_node!
    @pm = VueCli::Rails::NodeEnv.new
    abort('Cannot find node.js') unless @pm.node?
  end

  def package_manager?
    yarn = @pm.yarn_version
    npm = @pm.npm_version
    abort('Cannot find npm or yarn') unless yarn || npm

    input = if yarn && npm
      @input.gets(
        'Which package manager to use?',
        @root.join('package-lock.json').exist? ? 'yN' : 'Yn',
        y: 'yarn', n: 'npm',
      )
    else
      npm ? 'n' : 'y'
    end
    @pm.use!(input == 'n' ? :npm : :yarn)
    puts "Using package manager: #{@pm.package_manager}"
  end

  def install_vue_cli
    return if @pm.vue?

    puts "@vue/cli haven't been installed"
    pm.global_add('@vue/cli')
  end

  def run_vue_create?
    pack_input = 'y'
    if @pack.exist?
      puts 'Detected `package.json`!'
      pack_input = @input.gets(
        '  Do you want vue-cli to overwrite your package.json',
        'yAks',
        a: 'Auto', k: 'Keep', s: 'Skip vue create',
      )
    end
    return if pack_input == 's'

    gi_input = 'y'
    gi = @root.join('.gitignore')
    if gi.exist?
      puts 'Detected `.gitignore`!'
      gi_input = @input.gets(
        '  Do you want vue-cli to overwrite your .gitignore',
        'yMk',
        m: 'Merge', k: 'Keep',
      )
    end

    # backups
    vue_config = @root.join('vue.config.js')
    backup_vue_config = vue_config.exist?
    src_json = JSON.parse(@pack.read) if pack_input != 'y'
    gi_lines = gi.read.split("\n") if gi_input != 'y'
    if backup_vue_config
      FileUtils.mv(vue_config, "#{vue_config}.backup")
      # will be recreated
      FileUtils.rm_f(@root.join('vue.rails.js'))
    end
    pub_dir = @root.join('public')
    FileUtils.mv(pub_dir, "#{pub_dir}.backup")
    reademe = @root.join('README.md')
    reademe = reademe.exist? && reademe
    FileUtils.mv(reademe, "#{reademe}.backup") if reademe

    begin
      @pm.exec('vue create', '', "-n -m #{@pm.package_manager} .")
    ensure
      # restore backups
      FileUtils.rm_rf(pub_dir) if pub_dir.exist?
      FileUtils.mv("#{pub_dir}.backup", pub_dir)
      FileUtils.mv("#{vue_config}.backup", vue_config) if backup_vue_config
      if reademe
        FileUtils.rm_f(reademe)
        FileUtils.mv("#{reademe}.backup", reademe)
      end
    end

    # merge gitignore
    if gi_input == 'm'
      old_gi = Set.new(gi_lines.map(&:strip))
      new_lines = gi.read.split("\n")

      new_lines.map(&:strip).find_all(&:present?).each do |ln|
        gi_lines << ln unless old_gi.include?(ln)
      end
      gi.write(gi_lines.map { |ln| "#{ln}\n" }.join('')) if gi_lines.size != new_lines.size
    elsif gi_input == 'k'
      gi.write(gi_lines.join(''))
    end

    # check packages.json
    return if pack_input == 'y'
    dst_json = JSON.parse(@pack.read)
    return if dst_json == src_json

    # merge packages.json
    src_json, dst_json = [dst_json, src_json] if pack_input == 'a'
    dst_json.deep_merge!(src_json)
    @pack.write(JSON.pretty_generate(dst_json))
    @pending_install = true
  end

  def install_dev_deps
    package = JSON.parse(@pack.read)
    dev_deps = package['devDependencies']
    dd = %w[webpack-assets-manifest js-yaml].find_all do |dep|
      !dev_deps.key?(dep)
    end
    if dd.any?
      @pm.add("-D #{dd.join(' ')}")
      @pending_install = false
    end
  end

  def delete_vue_sample?
    %w[src dist].each do |fn|
      file = @root.join(fn)
      next unless file.exist?

      puts "Detected `#{fn}` (should be generated by vue-cli)"
      FileUtils.rm_rf(file.to_s) if @input.gets("  Do you want to delete #{file}?") == 'y'
    end
  end

  def copy_demo?
    return unless @input.gets('Do you want to copy demo code?', 'yN') == 'y'

    FileUtils.cp_r(@src_dir.join('app'), @root)
    routes = @root.join('config', 'routes.rb')
    return unless routes.exist?

    route_lines = routes.read.split("\n")
    foo = false
    bar = false
    route_lines.each do |line|
      foo ||= line.include?('vue_demo#foo')
      bar ||= line.include?('vue_demo#bar')
    end
    return if foo && bar

    end_line = route_lines.rindex { |ln| ln =~ /^\s*end\b/ }
    return if end_line.blank?

    route_lines.insert(end_line, "  get 'vue_demo/baz' => 'vue_demo#baz'") unless foo
    route_lines.insert(end_line, "  get 'vue_demo/bar' => 'vue_demo#bar'") unless bar
    route_lines.insert(end_line, "  get 'vue_demo/foo' => 'vue_demo#foo'") unless foo
    route_lines.insert(end_line, '  # Example of vue_cli-rails')
    routes.write(route_lines.join("\n"))
  end

  def copy_config
    puts 'Copying configuration files...'
    FileUtils.cp(@src_dir.join('vue.rails.js'), "#{@root}/")

    src_cfg = @src_dir.join('vue.config.js')
    dst_cfg = @root.join('vue.config.js')
    config = src_cfg.read
    if dst_cfg.exist?
      if dst_cfg.size > 500
        puts 'Detected `vue.config.js`!'
        return if @input.gets('  Do you want to overwrite vue.config.js?', 'yN') == 'n'
      else
        old_cfg = %x`node -e "console.log(JSON.stringify(require('./vue.config.js'),null,2))"`
        config = config.sub('const oldConfig = {};', "const oldConfig = #{old_cfg};")
      end
    end

    dst_cfg.write(config)
    @pending_fixes << dst_cfg.to_s
  end

  def generate_vue_yml
    yml_dest = @root.join('config', 'vue.yml')
    if yml_dest.exist?
      puts 'Detected `config/vue.yml`!'
      return if @input.gets('  Do you want to overwrite config/vue.yml?') == 'n'
    end

    yml = @src_dir.join('vue.yml').read
    yml = yml.sub('${PACKAGE_MANAGER}', @pm.package_manager.to_s)
    yml_dest.write(yml)
  end

  def ensure_entry_point!
    %w[entry_points assets components utils views].each do |dir_name|
      dir = @root.join("app/assets/vue/#{dir_name}")
      FileUtils.mkdir_p(dir) unless dir.exist?
    end

    ep_dir = @root.join('app/assets/vue/entry_points')
    return if Dir[ep_dir.join('**/*.js')].any?
    ep_dir.join('entry_point_demo.js').write("console.log('This is a demo entry point')")
  end

  def fix_jest_config!
    jest_file = @root.join('jest.config.js')
    return unless jest_file.exist?

    jest_config = %x`node -e "console.log(JSON.stringify(require('./jest.config.js')))"`
    jest_config = JSON.parse(jest_config)
    if jest_config['transformIgnorePatterns'] &&
       !jest_config['transformIgnorePatterns'].include?('<rootDir>/node_modules/')
      jest_config['transformIgnorePatterns'].unshift('<rootDir>/node_modules/')
    end
    jest_config.delete('moduleNameMapper')
    jest = <<~JS
      const { jestModuleNameMapper: moduleNameMapper } = require('./vue.rails');

      module.exports = #{JSON.pretty_generate(jest_config)}_MODULE_NAME_MAPPER_;
    JS
    jest_file.write(jest.sub(/\n?}_MODULE_NAME_MAPPER_/, ",\n  moduleNameMapper\n}"))
    @pending_fixes << jest_file.to_s
  rescue => e
    STDERR.puts e.message
  end

  def eslint_fix!
    dev_deps = JSON.parse(@pack.read)['devDependencies']
    return unless dev_deps['eslint'].present?
    @pm.exec('eslint', @pending_fixes.map { |fn| "'#{fn}'" }.join(' '), '--fix')
  rescue => e
    STDERR.puts e.message
  end
end
