#!/usr/bin/ruby

require 'pathname'
require 'fileutils'
require 'rubygems'
require 'thor'

require 'hrx'

class HRX::CLI < Thor
  desc "create <archive> <paths>...", "Create a new archive"
  option :root, aliases: :r, type: :string, banner: "Root of the archive"
  long_desc <<-LONGDESC
    Create an archive containing the given paths.

    The paths in the archive will be relative to the --root option, which
    defaults to the working directory.

    > hrx create spec.hrx input.scss output.css
  LONGDESC
  def create(destination, *paths)
    root = _normalize(Pathname.new(options[:root] || Dir.pwd))

    entries = []
    paths.each do |p|
      begin
        entries << _file_for(p, root)
      rescue SystemCallError
        raise Thor::Error.new("#{p} doesn't exist!") unless File.directory?(p)

        # If reading from a file fails, try listing a directory.
        Dir.glob("#{p}/**/*", File::FNM_DOTMATCH).each do |f|
          entries << _file_for(f, root) if File.file?(f)
        end
      end
    end

    entries.sort_by! {|e| e.path}
    HRX::Archive.new.tap do |a|
      entries.each {|e| a << e}
    end.write!(destination)
  rescue SystemCallError => e
    raise Thor::Error.new(e.message)
  end

  desc "extract <archive>", "Extract files from an archive"
  option :directory, aliases: :d, type: :string,
         banner: "Directory to which files are extracted"
  option :level, aliases: :l, type: :numeric,
         banner: "Extract child archives below this nesting level"
  long_desc <<-LONGDESC
    Extract the files from an archive.

    This puts the files in the directory given by the --directory option, which
    defaults to the name of the archive without ".hrx".

    > hrx extract test/specs.hrx

    Extracted 12 files to test/specs.

    ---

    With --level, this extracts directories below that level into new HRX files.
    For example, --level 2 extracts error/basic/input.scss and
    error/basic/output.css to error/basic.hrx.
  LONGDESC
  def extract(archive)
    directory = options[:directory] || begin
      unless archive.end_with?(".hrx")
        raise Thor::Error.new(
          "Can't determine a default output directory for #{archive}. Pass one " +
          "with --directory.")
      end
      directory = archive[0...-".hrx".length]
    end

    hrx = HRX::Archive.load(archive)
    if options[:level]
      if options[:level] < 1
        raise Thor::Error.new("--level must be greater than or equal to 2.")
      elsif !options[:level].is_a?(Integer)
        raise Thor::Error.new("--level must be an integer.")
      end

      files_extracted = hrx.entries.group_by do |e|
        dir = File.dirname(e.path)
        dir == "." ? [] : dir.split("/")[0...options[:level]]
      end.sum do |(components, entries)|
        if components.length < options[:level]
          _write_entries(directory, entries)
        else
          path = components.join("/")
          written = _write(
            "#{directory}/#{path}.hrx",
            hrx.child_archive(path).to_hrx)
          written ? 1 : 0
        end
      end
    else
      files_extracted = _write_entries(directory, hrx.entries)
    end

    $stderr.puts(shell.set_color(
      "Extracted #{_numberOf(files_extracted, 'file')} to #{directory}.",
      :green))
  rescue HRX::Error, SystemCallError => e
   raise Thor::Error.new(e.message)
  end

  def self.exit_on_failure?
    true
  end

  private

  # Returns a description of the number of `singular`s.
  #
  # `plural` defaults to `singular` with a trailing "s".
  def _numberOf(number, singular, plural: nil)
    "#{number} #{_pluralize(number, singular, plural: plural)}"
  end

  # Returns `singular` if `number` is `1`, and otherwise returns `plural`.
  #
  # `plural` defaults to `singular` with a trailing "s".
  def _pluralize(number, singular, plural: nil)
    number == 1 ? singular : (plural || "#{singular}s")
  end

  # Writes `contents` to disk at `path`, querying the user if it would overwrite
  # the existing contents. Creates the directories containing `path` if
  # necessary.
  #
  # Returns whether the file was actually written.
  def _write(path, contents)
    if !File.exist?(path) || file_collision(path) {contents}
      FileUtils.mkdir_p(File.dirname(path))
      File.write(path, contents)
      true
    else
      false
    end
  end

  # Returns the HRX version `path`, relative to `root`.
  def _relative_path_for(path, root)
    relative = _normalize(Pathname.new(path)).relative_path_from(root).to_s
    relative.gsub!("\\", "/") if Gem.win_platform?

    if relative.start_with?("../")
      raise Thor::Error.new("#{path} is not in #{options[:root] || 'the current directory'}")
    end

    relative
  end

  # Returns an HRX::File for the file at `path`, relative to `root`.
  def _file_for(path, root)
    HRX::File.new(
      _relative_path_for(path, root),
      File.read(path, encoding: 'binary'))
  rescue HRX::Error => e
    raise Thor::Error.new("Error archiving #{path}: #{e.message}")
  rescue EncodingError => e
    raise Thor::Error.new("Invalid UTF-8 in #{path}: #{e.message}")
  end

  # Writes each entry in `entries` to real files in `directory`.
  #
  # Returns the number of files written this way.
  def _write_entries(directory, entries)
    entries.count do |entry|
      destination = File.join(directory, entry.path)
      if entry.is_a?(HRX::Directory)
        FileUtils.mkdir_p(destination)
        false
      else
        _write(destination, entry.content)
      end
    end
  end

  # Ensures that `pathname` is absolute and doesn't contain `..`.
  def _normalize(pathname)
    return pathname.cleanpath if pathname.absolute?
    (Pathname.new(Dir.pwd) + pathname).cleanpath
  end
end

HRX::CLI.start(ARGV) if __FILE__ == $0