# The url class handles parsing and updating the url
require 'volt/reactive/reactive_accessors'
module Volt
  class URL
    include ReactiveAccessors

    # TODO: we need to make it so change events only trigger on changes
    reactive_accessor :scheme, :host, :port, :path, :query, :params, :fragment
    attr_accessor :router

    def initialize(router = nil)
      @router = router
      @params = Model.new({}, persistor: Persistors::Params)
    end

    # Parse takes in a url and extracts each sections.
    # It also assigns and changes to the params.
    def parse(url)
      if url[0] == '#'
        # url only updates fragment
        self.fragment = url[1..-1]
        update!
      else
        host     = `document.location.host`
        protocol = `document.location.protocol`

        if url !~ /[:]\/\//
          # Add the host for local urls
          url = protocol + "//#{host}" + url
        else
          # Make sure its on the same protocol and host, otherwise its external.
          if url !~ /#{protocol}\/\/#{host}/
            # Different host, don't process
            return false
          end
        end

        matcher         = url.match(/^(#{protocol[0..-2]})[:]\/\/([^\/]+)(.*)$/)
        self.scheme     = matcher[1]
        self.host, port = matcher[2].split(':')

        self.port = (port || 80).to_i

        path           = matcher[3]
        path, fragment = path.split('#', 2)
        path, query    = path.split('?', 2)

        self.path     = path
        self.fragment = fragment
        self.query    = query

        assign_query_hash_to_params
      end

      scroll

      true
    end

    # Full url rebuilds the url from it's constituent parts.
    # The params passed in are used to generate the urls.
    def url_for(params)
      host_with_port = host
      host_with_port += ":#{port}" if port && port != 80

      path, params = @router.params_to_url(params)

      new_url    = "#{scheme}://#{host_with_port}#{path.chomp('/')}"

      # Add query params
      params_str = ''
      unless params.empty?
        query_parts = []
        nested_params_hash(params).each_pair do |key, value|
          # remove the _ from the front
          value = `encodeURI(value)`
          query_parts << "#{key}=#{value}"
        end

        if query_parts.size > 0
          query = query_parts.join('&')
          new_url    += '?' + query
        end
      end

      # Add fragment
      frag    = fragment
      new_url += '#' + frag if frag.present?

      new_url
    end

    def url_with(params)
      url_for(@params.to_h.merge(params))
    end

    # Called when the state has changed and the url in the
    # browser should be updated
    # Called when an attribute changes to update the url
    def update!
      if Volt.client?
        new_url = url_for(@params.to_h)

        # Push the new url if pushState is supported
        # TODO: add fragment fallback
        `
        if (document.location.href != new_url && history && history.pushState) {
          history.pushState(null, null, new_url);
        }
      `
      end
    end

    def scroll
      if Volt.client?
        frag = fragment
        if frag.present?
          # Scroll to anchor via http://www.w3.org/html/wg/drafts/html/master/browsers.html#scroll-to-fragid
          `
          var anchor = $('#' + frag);
          if (anchor.length == 0) {
            anchor = $('*[name="' + frag + '"]:first');
          }
          if (anchor && anchor.length > 0) {
            console.log('scroll to: ', anchor.offset().top);
            $(document.body).scrollTop(anchor.offset().top);
          }
        `
        else
          # Scroll to the top by default
          `$(document.body).scrollTop(0);`
        end
      end
    end

    private

    # Assigning the params is tricky since we don't want to trigger changed on
    # any values that have not changed.  So we first loop through all current
    # url params, removing any not present in the params, while also removing
    # them from the list of new params as added.  Then we loop through the
    # remaining new parameters and assign them.
    def assign_query_hash_to_params
      # Get a nested hash representing the current url params.
      query_hash = self.query_hash

      # Get the params that are in the route
      new_params = @router.url_to_params(path)

      if new_params == false
        fail "no routes match path: #{path}"
      end

      query_hash.merge!(new_params)

      # Loop through the .params we already have assigned.
      assign_from_old(@params, query_hash)
      assign_new(@params, query_hash)
    end

    # Loop through the old params, and overwrite any existing values,
    # and delete the values that don't exist in the new params.  Also
    # remove any assigned to the new params (query_hash)
    def assign_from_old(params, new_params)
      queued_deletes = []

      params.attributes.each_pair do |name, old_val|
        # If there is a new value, see if it has [name]
        new_val = new_params ? new_params[name] : nil

        if !new_val
          # Queues the delete until after we finish the each_pair loop
          queued_deletes << name
        elsif new_val.is_a?(Hash)
          assign_from_old(old_val, new_val)
        else
          # assign value
          if old_val != new_val
            params.set(name, new_val)
          end
          new_params.delete(name)
        end
      end

      queued_deletes.each { |name| params.delete(name) }
    end

    # Assign any new params, which weren't in the old params.
    def assign_new(params, new_params)
      new_params.each_pair do |name, value|
        if value.is_a?(Hash)
          assign_new(params.get(name), value)
        else
          # assign
          params.set(name, value)
        end
      end
    end

    def query_hash
      query_hash = {}
      qury       = query
      if qury
        qury.split('&').reject { |v| v == '' }.each do |part|
          parts    = part.split('=').reject { |v| v == '' }

          # Decode string
          # parts[0] = `decodeURI(parts[0])`
          parts[1] = `decodeURI(parts[1])`

          sections = query_key_sections(parts[0])

          hash_part = query_hash
          sections.each_with_index do |section, index|
            if index == sections.size - 1
              # Last part, assign the value
              hash_part[section] = parts[1]
            else
              hash_part = (hash_part[section] ||= {})
            end
          end
        end
      end

      query_hash
    end

    # Splits a key from a ?key=value&... parameter into its nested
    # parts.  It also adds back the _'s used to access them in params.
    # Example:
    # user[name]=Ryan would parse as [:_user, :_name]
    def query_key_sections(key)
      key.split(/\[([^\]]+)\]/).reject(&:empty?)
    end

    # Generate the key for a nested param attribute
    def query_key(path)
      i = 0
      path.map do |v|
        i += 1
        if i != 1
          "[#{v}]"
        else
          v
        end
      end.join('')
    end

    def nested_params_hash(params, path = [])
      results = {}

      params.each_pair do |key, value|
        unless value.nil?
          if value.respond_to?(:persistor) && value.persistor && value.persistor.is_a?(Persistors::Params)
            # TODO: Should be a param
            results.merge!(nested_params_hash(value, path + [key]))
          else
            results[query_key(path + [key])] = value
          end
        end
      end

      results
    end
  end
end