require 'checksum'
require 'set'

module Cask::DSL
  def self.included(base)
    base.extend(ClassMethods)
  end

  def homepage; self.class.homepage; end

  def url; self.class.url; end

  def appcast; self.class.appcast; end

  def gpg; self.class.gpg; end

  def version; self.class.version; end

  def license; self.class.license; end

  def depends_on_formula; self.class.depends_on_formula; end

  def depends_on; self.class.depends_on; end

  def container_type; self.class.container_type; end

  def sums; self.class.sums || []; end

  def artifacts; self.class.artifacts; end

  def caveats; self.class.caveats; end

  module ClassMethods
    def homepage(homepage=nil)
      if @homepage and !homepage.nil?
        raise CaskInvalidError.new(self.title, "'homepage' stanza may only appear once")
      end
      @homepage ||= homepage
    end

    def url(*args)
      if @url and !args.empty?
        raise CaskInvalidError.new(self.title, "'url' stanza may only appear once")
      end
      @url ||= begin
        Cask::URL.new(*args) unless args.empty?
      end
    end

    def appcast(*args)
      if @appcast and !args.empty?
        raise CaskInvalidError.new(self.title, "'appcast' stanza may only appear once")
      end
      @appcast ||= begin
        Cask::Appcast.new(*args) unless args.empty?
      rescue StandardError => e
        raise CaskInvalidError.new(self.title, e)
      end
    end

    def gpg(*args)
      if @gpg and !args.empty?
        raise CaskInvalidError.new(self.title, "'gpg' stanza may only appear once")
      end
      @gpg ||= begin
        Cask::Gpg.new(*args) unless args.empty?
      rescue StandardError => e
        raise CaskInvalidError.new(self.title, e)
      end
    end

    def container_type(type=nil)
      if @container_type and !type.nil?
        raise CaskInvalidError.new(self.title, "'container_type' stanza may only appear once")
      end
      @container_type ||= type
    end

    SYMBOLIC_VERSIONS = Set.new [
      :latest,
    ]

    def version(arg=nil)
      if arg.nil?
        @version
      elsif @version
        raise CaskInvalidError.new(self.title, "'version' stanza may only appear once")
      elsif !arg.is_a?(String) and !SYMBOLIC_VERSIONS.include?(arg)
        raise CaskInvalidError.new(self.title, "invalid 'version' value: '#{arg.inspect}'")
      end
      @version ||= arg
    end

    def license(arg=nil)
      if @license and !arg.nil?
        raise CaskInvalidError.new(self.title, "'license' stanza may only appear once")
      end
      @license ||= begin
        Cask::License.new(arg) unless arg.nil?
      rescue StandardError => e
        raise CaskInvalidError.new(self.title, e)
      end
    end

    def depends_on_formula(*args)
      @depends_on_formula ||= args
    end

    def depends_on(*args)
      if @depends_on and !args.empty?
        # todo: remove this constraint, and instead merge multiple depends_on stanzas
        raise CaskInvalidError.new(self.title, "'depends_on' stanza may only appear once")
      end
      @depends_on ||= begin
        Cask::DependsOn.new(*args) unless args.empty?
      rescue StandardError => e
        raise CaskInvalidError.new(self.title, e)
      end
      # todo: remove this backwards compatibility section after removing depends_on_formula
      if @depends_on.formula
        @depends_on_formula ||= @depends_on.formula
      end
      @depends_on
    end

    def artifacts
      @artifacts ||= Hash.new { |hash, key| hash[key] = Set.new }
    end

    def caveats(*string, &block)
      @caveats ||= []
      if block_given?
        @caveats << Cask::Caveats.new(block)
      elsif string.any?
        @caveats << string.map{ |s| s.to_s.sub(/[\r\n \t]*\Z/, "\n\n") }
      else
        # accessor
        @caveats
      end
    end

    # This hash is transitional.  Each of these stanzas will
    # ultimately either be removed or upgraded with its own
    # unique semantics.
    STANZA_ALIASES = {
                       :pkg                   => :install,          # to remove
                       :app                   => :link,             # to upgrade
                       :suite                 => :link,             # to upgrade
                       :preflight             => :before_install,   # to remove
                       :postflight            => :after_install,    # to remove
                       :uninstall_preflight   => :before_uninstall, # to remove
                       :uninstall_postflight  => :after_uninstall,  # to remove
                     }

    def self.ordinary_artifact_types
      @@ordinary_artifact_types ||= [
                                     :link,
                                     :app,
                                     :suite,
                                     :prefpane,
                                     :qlplugin,
                                     :font,
                                     :widget,
                                     :service,
                                     :colorpicker,
                                     :binary,
                                     :input_method,
                                     :screen_saver,
                                     :install,      # deprecated
                                     :pkg,
                                    ]
    end

    installable_artifact_types = ordinary_artifact_types
    installable_artifact_types.push :caskroom_only

    installable_artifact_types.each do |type|
      resolved_type = STANZA_ALIASES.key?(type) ? STANZA_ALIASES[type] : type
      define_method(type) do |*args|
        artifacts[resolved_type] << args
      end
    end

    SPECIAL_ARTIFACT_TYPES = [
      :nested_container,
      :uninstall,
      :zap,
    ]

    SPECIAL_ARTIFACT_TYPES.each do |type|
      define_method(type) do |*args|
        artifacts[type].merge(args)
      end
    end

    ARTIFACT_BLOCK_TYPES = [
      :after_install,           # deprecated
      :after_uninstall,         # deprecated
      :before_install,          # deprecated
      :before_uninstall,        # deprecated
      :preflight,
      :postflight,
      :uninstall_preflight,
      :uninstall_postflight,
    ]

    ARTIFACT_BLOCK_TYPES.each do |type|
      resolved_type = STANZA_ALIASES.key?(type) ? STANZA_ALIASES[type] : type
      define_method(type) do |&block|
        artifacts[resolved_type] << block
      end
    end

    def install_script(*args)
      executable = args.shift
      args = Hash.new(*args)
      args.merge!({ :executable => executable })
      artifacts[:install_script] << args
    end

    attr_reader :sums

    def hash_name(hash_type)
      hash_type.to_s == 'sha2' ? 'sha256' : hash_type.to_s
    end

    def sha256(sha2=nil)
      if sha2 == :no_check
        @sums = sha2
      else
        @sums ||= []
        @sums << Checksum.new(:sha2, sha2) unless sha2.nil?
      end
    end

    def method_missing(method, *args)
      poo = <<-EOPOO.undent
        Unexpected method '#{method}' called on #{self}.

          If you are working on #{self}, this may point to a typo. Otherwise
          it probably means this Cask is using a new feature. If that feature
          has been released, running

            brew update && brew upgrade brew-cask && brew cleanup && brew cask cleanup

          should fix it. Otherwise you should wait to use #{self} until the
          new feature is released.
      EOPOO
      poo.split("\n").each { |line| opoo line }
    end
  end
end
