module HAR
  class Archive
    include Serializable

    def self.from_string(str, uri = nil)
      new JSON.parse(str), uri
    end

    def self.from_file(path_or_io)
      case path_or_io
      when String
        from_string File.read(path_or_io), path_or_io
      when IO
        from_string path_or_io.read, path_or_io.to_s
      else
        unless path_or_io.respond_to?(:to_io)
          raise TypeError, "expected String, IO or #to_io"
        end

        from_file path_or_io.to_io
      end
    end

    def self.by_merging(hars)
      hars = hars.dup

      result = hars.shift or raise ArgumentError, "no HARs given"
      result = from_file(result) unless result.kind_of? self

      hars.each do |har|
        result.merge! har.kind_of?(self) ? har : from_file(har)
      end

      result
    end

    # @api private
    def self.schemas
      @schemas ||= {}
    end

    # @api private
    def self.add_schema(path)
      data = JSON.parse(File.read(path))
      id = data.fetch('id')

      schemas[id] = data
    end

    Dir[File.expand_path("../schemas/*.json", __FILE__)].each do |path|
      add_schema path
    end

    attr_reader :uri

    def initialize(input, uri = nil)
      @data = input
      @uri   = uri
    end

    def view
      Viewer.new([self]).show
    end

    def pages
      @pages ||= raw_log.fetch('pages').map { |page|
        Page.new page, entries_for(page['id'])
      }
    end

    def entries
      @entries ||= raw_entries.map { |e| Entry.new(e) }
    end

    def entries_before(time)
      raise TypeError, "expected Time" unless time.is_a?(Time)
      entries.select do |entry|
        next(false) unless entry.time
        entry.started_date_time + entry.time / 1000.0 <= time
      end
    end

    # create a new archive by merging this and another archive

    def merge(other)
      assert_archive other

      data = deep_clone(@data)
      merge_data data, other.as_json, other.uri

      self.class.new data
    end

    # destructively merge this with the given archive

    def merge!(other)
      assert_archive other
      clear_caches

      merge_data @data, other.as_json, other.uri
      nil
    end

    def save_to(path)
      File.open(path, "w") { |io| io << @data.to_json }
    end

    def valid?
      Jschematic.validate @data, log_type_schema, :debug => true, :context => self.class.schemas.values
    end

    def validate!
      Jschematic.validate! @data, log_type_schema, :debug => true, :context => self.class.schemas.values
      nil
    rescue Jschematic::ValidationError => ex
      msg = ex.message
      msg = "#{@uri}: #{msg}" if @uri

      raise ValidationError, msg
    end

    private

    def log_type_schema
      @schema ||= self.class.schemas.fetch('logType')
    end

    def merge_data(left, right, uri)
      log       = left.fetch('log')
      other_log = right.fetch('log')

      pages   = log.fetch('pages')
      entries = log.fetch('entries')

      other_pages = other_log.fetch("pages")
      other_entries = other_log.fetch("entries")

      if uri
        deep_clone(other_pages).each do |page|
          c = page['comment'] ||= ''
          c << "(merged from #{File.basename uri})"

          pages << page
        end
      else
        pages.concat other_pages
      end

      entries.concat other_entries
    end

    def assert_archive(other)
      unless other.kind_of?(self.class)
        raise TypeError, "expected #{self.class}"
      end
    end

    def clear_caches
      @raw_entries = @entries_map = @pages = @entries = @log = nil
    end

    def entries_for(page_id)
      entries = entries_map[page_id] || []
      entries.map { |e| Entry.new(e) }
    end

    def raw_entries
      @raw_entries ||= raw_log.fetch('entries')
    end

    def raw_log
      @raw_log ||= @data.fetch 'log'
    end

    def entries_map
      @entries_map ||= raw_entries.group_by { |e| e['pageref'] }
    end

    def deep_clone(obj)
      Marshal.load Marshal.dump(obj)
    end

  end # Archive
end # HAR
