require 'fileutils'
require 'zip/zip'

# Just a simple helper class to export quickly an existing and live locomotive website.
# FIXME: will be replaced by the API in the next months
module Locomotive
  class Export

    @@instance = nil

    def initialize(site, filename = nil)
      @site           = site
      @filename       = filename || Time.now.to_i.to_s
      @target_folder  = File.join(Rails.root, 'tmp', 'export', @filename)
      @site_hash      = {} # used to generate the site.yml and compiled_site.yml files

      self.create_target_folder
    end

    def run!
      self.initialize_site_hash

      self.log('copying content assets')
      self.copy_content_assets

      self.log('copying theme assets')
      self.copy_theme_assets

      self.log('copying pages')
      self.copy_pages

      self.log('copying snippets')
      self.copy_snippets

      self.log('copying content types')
      self.copy_content_types

      self.log('copying config files')
      self.copy_config_files

      self.log('generating the zip file')
      self.zip_it!
    end

    # returns the path to the zipfile
    def self.run!(site, filename = nil)
      @@instance = self.new(site, filename)
      @@instance.run!
    end

    protected

    def zip_it!
      "#{@target_folder}.zip".tap do |dst|
        FileUtils.rm(dst, :force => true)
        ::Zip::ZipFile.open(dst, ::Zip::ZipFile::CREATE) do |zipfile|
          Dir[File.join(@target_folder, '**/*')].each do |file|
            entry = file.gsub(@target_folder + '/', '')
            zipfile.add(entry, file)
          end
        end
      end
    end

    def create_target_folder
      FileUtils.rm_rf(@target_folder)
      FileUtils.mkdir_p(@target_folder)
      %w(app/content_types app/views/snippets app/views/pages config data public).each do |f|
        FileUtils.mkdir_p(File.join(@target_folder, f))
      end
    end

    def initialize_site_hash
      attributes = self.extract_attributes(@site, %w(name locale seo_title meta_description meta_keywords))

      attributes['pages'] = []
      attributes['content_types'] = {}

      @site_hash = { 'site' => attributes }
    end

    def copy_config_files
      File.open(File.join(self.config_folder, 'compiled_site.yml'), 'w') do |f|
        f.write(yaml(@site_hash))
      end

      @site_hash['site'].delete('content_types')

      File.open(File.join(self.config_folder, 'site.yml'), 'w') do |f|
        f.write(yaml(@site_hash))
      end
    end

    def copy_pages
      Page.quick_tree(@site, false).each { |p| self._copy_pages(p) }
    end

    def copy_snippets
      @site.snippets.each do |snippet|
        File.open(File.join(self.snippets_folder, "#{snippet.slug}.liquid"), 'w') do |f|
          f.write(snippet.template)
        end
      end
    end

    def copy_content_types
      @site.content_types.each do |content_type|
        attributes = self.extract_attributes(content_type, %w(name description slug order_by order_direction group_by_field_name api_enabled))

        attributes['highlighted_field_name'] = content_type.highlighted_field.name

        # TODO: refactoring

        # custom_fields
        fields = []
        content_type.entries_custom_fields.each do |field|
          field_attributes = self.extract_attributes(field, %w(label type hint required))

          if field.target.present?
            target_klass = field['target'].constantize

            field_attributes['target'] = target_klass._parent.slug

            if field['reverse_lookup'].present?
              field_attributes['reverse'] = target_klass.custom_field_name_to_alias(field['reverse_lookup'])
            end
          end

          fields << { field.name => field_attributes }
        end

        attributes['fields'] = fields

        @site_hash['site']['content_types'][content_type.name] = attributes.clone

        # [structure] copy it into its own file
        File.open(File.join(self.content_types_folder, "#{content_type.slug}.yml"), 'w') do |f|
          f.write(self.yaml(attributes))
        end

        # data
        data = self.extract_contents(content_type)

        # [data] copy them into their own file
        File.open(File.join(self.content_data_folder, "#{content_type.slug}.yml"), 'w') do |f|
          f.write(self.yaml(data))
        end

        @site_hash['site']['content_types'][content_type.name]['contents'] = data
      end
    end

    def copy_theme_assets
      @site.theme_assets.each do |theme_asset|
        target_path = File.join(self.public_folder, theme_asset.local_path)

        self.copy_file_from_an_uploader(theme_asset.source, target_path) do |bytes|
          if theme_asset.stylesheet_or_javascript?
            base_url = theme_asset.source.url.gsub(theme_asset.local_path, '')
            bytes.gsub(base_url, '/')
          else
            bytes
          end
        end
      end
    end

    def copy_content_assets
      @site.content_assets.each do |content_asset|
        target_path = File.join(self.samples_folder, content_asset.source_filename)
        self.copy_file_from_an_uploader(content_asset.source, target_path)
      end
    end

    protected

    def _copy_pages(page)
      attributes = self.extract_attributes(page, %w{title seo_title meta_description meta_keywords redirect_url content_type published})

      attributes['listed'] = page.listed? # in some cases, page.listed can be nil

      unless page.raw_template.blank?
        attributes.delete('redirect_url')

        if page.templatized?
          attributes['content_type'] = page.content_type.slug
        end

        # add editable elements
        page.editable_elements.each do |element|
          next if element.disabled? || (element.from_parent && element.default_content == element.content)

          el_attributes = self.extract_attributes(element, %w{slug block hint})

          case element
          when EditableShortText, EditableLongText
            el_attributes['content'] = self.replace_asset_urls_in(element.content || '')
          when EditableFile
            unless element.source_filename.blank?
              filepath = File.join('/', 'samples', element.source_filename)
              self.copy_file_from_an_uploader(element.source, File.join(self.public_folder, filepath))
              el_attributes['content'] = filepath
            else
              el_attributes['content'] = '' # not sure if we run into this
            end
          end

          (attributes['editable_elements'] ||= []) << el_attributes
        end

        page_templatepath = File.join(self.pages_folder, "#{page.fullpath}.liquid")

        FileUtils.mkdir_p(File.dirname(page_templatepath))

        File.open(page_templatepath, 'w') do |f|
          f.write(self.replace_asset_urls_in(page.raw_template))
        end
      end

      @site_hash['site']['pages'] << { page.fullpath => attributes }

      page.children.each { |p| self._copy_pages(p) }
    end

    def extract_attributes(object, fields)
      attributes = object.attributes.select { |k, v| fields.include?(k) && !v.blank? }

      if RUBY_VERSION =~ /1\.9/
        attributes
      else
        attributes.inject({}) { |memo, pair| memo.merge(pair.first => pair.last) }
      end
    end

    def pages_folder
      File.join(@target_folder, 'app', 'views', 'pages')
    end

    def snippets_folder
      File.join(@target_folder, 'app', 'views', 'snippets')
    end

    def content_types_folder
      File.join(@target_folder, 'app', 'content_types')
    end

    def config_folder
      File.join(@target_folder, 'config')
    end

    def content_data_folder
      File.join(@target_folder, 'data')
    end

    def public_folder
      File.join(@target_folder, 'public')
    end

    def samples_folder
      File.join(@target_folder, 'public', 'samples')
    end

    def copy_file_from_an_uploader(uploader, target_path, &block)
      FileUtils.mkdir_p(File.dirname(target_path))
      File.open(target_path, 'w') do |f|
        bytes = uploader.read

        bytes ||= ''

        bytes = block.call(bytes) if block_given?

        bytes = bytes.force_encoding('UTF-8') if RUBY_VERSION =~ /1\.9/
        f.write(bytes)
      end
    end

    def extract_contents(content_type)
      data = []

      # TODO (refactoring....)

      highlighted_field_name = content_type.highlighted_field_name

      content_type.entries.each do |content|
        hash = {}

        content.custom_fields.each do |field|
          next if field._name == highlighted_field_name

          value = (case field.type
          when 'file'
            uploader = content.send(field._name)
            unless uploader.blank?
              filepath = File.join('/samples', content_type.slug, content.send("#{field._name}_filename"))
              self.copy_file_from_an_uploader(uploader, File.join(self.public_folder, filepath))
            else
              filepath = nil
            end
            filepath
          when 'text'
            self.replace_asset_urls_in(content.send(field._name.to_sym) || '')
          when 'has_one'
            content.send(field.name.to_sym).highlighted_field_value rescue nil # no bound object
          when 'has_many'
            unless field.reverse_has_many?
              content.send(field.name.to_sym).collect(&:highlighted_field_value)
            end
          else
            content.send(field.name.to_sym)
          end)

          hash[field.name] = value if value.present? || value == false # False values should be preserved in the export
        end

        data << { content.highlighted_field_value => hash }
      end

      data
    end

    def replace_asset_urls_in(text)
      base_url = ContentAssetUploader.new(ContentAsset.new(:site => @site)).store_dir
      (base_url = base_url.split('/')).pop
      base_url = base_url.join('/')

      text.gsub(%r(#{base_url}/[a-z0-9]{10,}/), "#{base_url.starts_with?('http') ? '/' : ''}samples/")
    end

    def yaml(hash_or_array)
      method = hash_or_array.respond_to?(:ya2yaml) ? :ya2yaml : :to_yaml
      string = (if hash_or_array.respond_to?(:keys)
        hash_or_array.dup.stringify_keys!
      else
        hash_or_array
      end).send(method)
      string.gsub('!ruby/symbol ', ':').sub('---', '').split("\n").map(&:rstrip).join("\n").strip
    end

    def log(message, domain = '')
      head = "[export_template][#{@site.name}]"
      head += "[#{domain}]" unless domain.blank?
      ::Locomotive.log "\t#{head} #{message}"
    end

  end

end