require 'time'
require 'uri'
require_relative 'html_truncater'

module Liquor
  module Builtins
    #
    # Universal functions
    #

    function "is_empty", unnamed_arg: :any do |arg,|
      arg.nil? || (arg.respond_to?(:size) && arg.size == 0)
    end

    function "size", unnamed_arg: [:string, :tuple] do |arg,|
      arg.size
    end

    #
    # Type conversion functions
    #

    function "strftime",
              unnamed_arg: :string,
              mandatory_named_args: { format: :string } do |arg, kw|
      begin
        time = Time.parse(arg)

        if defined?(I18n)
          I18n.localize(time, format: kw[:format])
        else
          time.strftime(kw[:format])
        end

      rescue Exception => e
        arg
      end
    end

    function "to_number", unnamed_arg: [:integer, :string] do |arg,|
      if arg.is_a? Integer
        arg
      else
        arg.to_i
      end
    end

    #
    # Integer functions
    #

    function "is_even", unnamed_arg: :integer do |arg,|
      (arg % 2) == 0
    end

    function "is_odd", unnamed_arg: :integer do |arg,|
      (arg % 2) == 1
    end

    #
    # String functions
    #

    function "downcase", unnamed_arg: :string do |arg,|
      arg.downcase
    end

    function "upcase", unnamed_arg: :string do |arg,|
      arg.upcase
    end

    function "capitalize", unnamed_arg: :string do |arg,|
      arg.capitalize
    end

    function "starts_with", unnamed_arg: :string,
                            mandatory_named_args: { pattern: :string } do |arg, kw|
      arg.start_with?(kw[:pattern])
    end

    function "strip_newlines", unnamed_arg: :string do |arg,|
      arg.gsub("\n", "")
    end

    function "join", unnamed_arg: :tuple,
                     optional_named_args: { with: :string } do |arg, kw|
      arg.flatten.join(kw[:with] || ' ')
    end

    function "split", unnamed_arg: :string,
                      optional_named_args: { by: :string } do |arg, kw|
      arg.split(kw[:by] || ' ')
    end

    function "replace",
              unnamed_arg: :string,
              mandatory_named_args: {
                pattern: :string,
                replacement: :string
              } do |arg, kw|
      arg.gsub kw[:pattern], kw[:replacement]
    end

    function "replace_first",
              unnamed_arg: :string,
              mandatory_named_args: {
                pattern: :string,
                replacement: :string
              } do |arg, kw|
      arg.sub kw[:pattern], kw[:replacement]
    end

    function "remove",
              unnamed_arg: :string,
              mandatory_named_args: {
                pattern: :string,
              } do |arg, kw|
      arg.gsub kw[:pattern], ''
    end

    function "remove_first",
              unnamed_arg: :string,
              mandatory_named_args: {
                pattern: :string,
              } do |arg, kw|
      arg.sub kw[:pattern], ''
    end

    function "newline_to_br", unnamed_arg: :string do |arg,|
      arg.gsub(/\n/, "<br>\n")
    end

    function "url_escape", unnamed_arg: :string do |arg,|
      URI.encode_www_form_component(arg)
    end

    function "html_escape", unnamed_arg: [:string, :null] do |arg,|
      if arg
        if defined?(Rack::Utils)
          Rack::Utils.escape_html(arg)
        else
          raise NotImplementedError, "html_escape() requires Rack"
        end
      end
    end

    function "html_escape_once", unnamed_arg: :string do |arg,|
      if defined?(ActionView)
        @html_escape_once_helper ||= Object.new.
            tap { |obj| obj.extend ActionView::Helpers::TagHelper }
        @html_escape_once_helper.escape_once(arg)
      else
        raise NotImplementedError, "html_escape_once() requires ActionView"
      end
    end
    function_alias "h", "html_escape_once"

    function "strip_html", unnamed_arg: :string do |arg,|
      if defined?(HTML::Sanitizer)
        HTML::FullSanitizer.new.sanitize(arg)
      else
        raise NotImplementedError, "escape_once() requires HTML::Sanitizer (try loading ActionView)"
      end
    end

    function "decode_html_entities", unnamed_arg: :string do |arg,|
      if defined?(HTMLEntities)
        HTMLEntities.new.decode(arg)
      else
        raise NotImplementedError, "decode_html_entities() requires HTMLEntities"
      end
    end

    #
    # Tuple functions
    #

    function "compact", unnamed_arg: :tuple do |arg,|
      arg.compact
    end

    function "reverse", unnamed_arg: :tuple do |arg,|
      arg.reverse
    end

    function "uniq", unnamed_arg: :tuple do |arg, |
      arg.uniq
    end

    minmax = lambda do |fn, fn_by, set, key|
      if key
        set.to_a.select { |r| !r.liquor_send(key, []).nil? }.
                 send(fn_by) { |r| r.liquor_send(key, []) }
      else
        set.to_a.send(fn)
      end
    end

    function "min", unnamed_arg: [:tuple],
                    optional_named_args: {
                      by: :string
                    } do |source_set, kw|
      minmax.call(:min, :min_by, source_set, kw[:by])
    end

    function "max", unnamed_arg: [:tuple],
                    optional_named_args: {
                      by: :string
                    } do |source_set, kw|
      minmax.call(:max, :max_by, source_set, kw[:by])
    end

    function "in_groups_of",
              unnamed_arg: [:tuple],
              mandatory_named_args: { size: :integer },
              optional_named_args:  { fill_with: [:string, :boolean] } do |arg, kw|
      if [].respond_to? :in_groups_of
        arg.to_a.in_groups_of(kw[:size], kw[:fill_with])
      else
        raise NotImplementedError, "in_groups_of() requires ActiveSupport Array extensions"
      end
    end

    function "in_groups",
              unnamed_arg: [:tuple],
              mandatory_named_args: { count: :integer },
              optional_named_args:  { fill_with: [:string, :boolean] } do |arg, kw|
      if [].respond_to? :in_groups
        arg.to_a.in_groups(kw[:count], kw[:fill_with])
      else
        raise NotImplementedError, "in_groups() requires ActiveSupport Array extensions"
      end
    end

    function "includes",
              unnamed_arg: [:tuple, :external],
              mandatory_named_args: { element: :any } do |arg, kw|
      if arg.respond_to? :include?
        arg.include?(kw[:element])
      else
        raise Liquor::TypeError.new("includes() requires an external that exports include? method")
      end
    end

    function "index_of",
             unnamed_arg: [:tuple, :external],
             mandatory_named_args: { element: :any } do |arg, kw|
      if arg.respond_to? :index
        arg.index(kw[:element])
      else
        raise Liquor::TypeError.new("includes() requires an external that exports index method")
      end
    end

    #
    # Intelligent truncation functions
    #

    function "truncate",
              unnamed_arg: :string,
              optional_named_args: {
                length:   :integer,
                omission: :string
              } do |arg, kw|
      length   = kw[:length]   || 50
      omission = kw[:omission] || '...'

      truncate_at = length - kw[:omission].length
      truncate_at = 0 if length < 0

      if arg.length > length
        arg[0...truncate_at] + omission
      else
        arg
      end
    end

    function "truncate_words",
              unnamed_arg: :string,
              optional_named_args: {
                words:    :integer,
                omission: :string
              } do |arg, kw|
      words    = arg.split
      length   = kw[:words]    || 15
      omission = kw[:omission] || '...'

      if words.length > length
        words[0...length].join(" ") + omission
      else
        words.join(" ")
      end
    end

    function "html_truncate",
              unnamed_arg: :string,
              optional_named_args: {
                length:   :integer,
                omission: :string
              } do |arg, kw|
      length   = kw[:length]   || 50
      omission = kw[:omission] || '...'

      if defined?(Nokogiri)
        HTMLTruncater.truncate(arg, length, omission)
      else
        raise NotImplementedError, "html_truncate() requires Nokogiri"
      end
    end

    function "html_truncate_words",
              unnamed_arg: :string,
              optional_named_args: {
                words:    :integer,
                omission: :string
              } do |arg, kw|
      length   = kw[:words]    || 15
      omission = kw[:omission] || '...'

      if defined?(Nokogiri)
        HTMLTruncater.truncate_words(arg, length, omission)
      else
        raise NotImplementedError, "html_truncate_words() requires Nokogiri"
      end
    end

  end
end
