# -*- encoding: utf-8 -*-

# Standard lib requires
require 'stringio'
require 'fileutils'
require 'ostruct'
require 'date'

# Requirements for Website
require 'webgen/core_ext'
require 'webgen/utils'
require 'webgen/configuration'
require 'webgen/blackboard'
require 'webgen/cache'
require 'webgen/error'
require 'webgen/tree'
require 'webgen/bundle_loader'
require 'webgen/logger'

# Load other needed files
require 'webgen/path'
require 'webgen/node'
require 'webgen/page'
require 'webgen/version'


module Webgen

  #
  # == About
  #
  # Represents a webgen website and provides the main interface for users.
  #
  # Normally, webgen is used from the command line via the +webgen+ command or from Rakefiles via
  # Webgen::RakeTask. However, you can also easily use webgen as a library and this class provides
  # the interface for this usage!
  #
  # You may notice that this class doesn't have many methods. This is because webgen is designed
  # from ground up to be extensible. Most of the 'magic' happens in extensions which are registered
  # on the #ext OpenStruct object. The simple 'core' classes that are not extensions have separate
  # accessor methods (#config for the Configuration object, #blackboard for the Blackboard and so
  # on).
  #
  # Since a webgen website is, basically, just a directory, the only parameter needed for creating a
  # new Website object is the website directory. Once created, the website is fully initialized and
  # one can work with it:
  #
  # * If you want to generate the website, you just need to call #execute_task with
  #   :generate_website as parameter.
  #
  # * If you want to retrieve data from the website, you can use the various accessors on the
  #   Website object itself or use #ext to access all available extensions.
  #
  #   *Note*: This is generally only useful if the website has been generated before because
  #   otherwise there probably is no data to retrieve.
  #
  class Website

    # The website configuration.
    attr_reader :config

    # The blackboard used for inter-object communication.
    attr_reader :blackboard

    # A cache to store information that should be available the next time the website gets
    # generated.
    attr_reader :cache

    # Access to all extension objects. An OpenStruct object.
    attr_reader :ext

    # The internal data structure used to store information about individual nodes.
    #
    # See Tree for more information
    attr_reader :tree

    # The Webgen::Logger used for logging.
    attr_reader :logger

    # The website directory.
    attr_reader :directory

    # Create a new webgen Website object for the website in the directory +dir+.
    #
    # If no logger is specified, a dummy logger that logs to a StringIO is created.
    #
    # You can provide a block for modifying the Website object in any way during the initialization:
    #
    # * If the block only takes one parameter, it is called with the Website object after the
    #   initialization is done but before the cache is restored.
    #
    # * If it takes two parameters, the first one is the Website object and the second one is a
    #   boolean specifying whether the block is currently called any initialization (value is
    #   +true+) or after it (value is +false).
    #
    def initialize(dir, logger = nil, &block)
      @directory = dir
      @logger = logger || Webgen::Logger.new(StringIO.new)
      @init_block = block
      init
    end

    # Initialize the configuration, blackboard and cache objects and load the default configuration
    # as well as all specified extensions.
    def init
      @tree = Tree.new(self)
      @blackboard = Blackboard.new
      @config = Configuration.new
      @cache = nil
      @ext = OpenStruct.new

      @init_block.call(self, true) if @init_block && @init_block.arity == 2
      loader = load_bundles
      load_configuration(loader)
      if @init_block
        @init_block.arity == 1 ? @init_block.call(self) : @init_block.call(self, false)
      end
      @config.freeze

      restore_cache
      @blackboard.dispatch_msg(:website_initialized)
    end
    private :init

    # Load all extension bundles.
    #
    # This loads the extension bundle for the built-in extensions as well as all website specific
    # extension bundles.
    def load_bundles
      ext_dir = File.join(@directory, 'ext')
      ext_loader = BundleLoader.new(self, ext_dir)
      ext_loader.load('built-in')
      ext_loader.load_autoload_bundles
      Dir[File.join(ext_dir, '**/init.rb')].sort.each {|file| ext_loader.load(file[ext_dir.length..-1])}
      ext_loader.load('init.rb') if File.file?(File.join(ext_dir, 'init.rb'))
      ext_loader
    end
    private :load_bundles

    # The name of the configuration file webgen uses.
    CONFIG_FILENAME = 'webgen.config'

    # Load the configuration file into the Configuration object.
    #
    # If it is a Ruby configuration file, the given bundle loader is used to load it.
    def load_configuration(bundle_loader)
      config_file = File.join(@directory, CONFIG_FILENAME)
      return unless File.exist?(config_file)

      first_line = File.open(config_file, 'r') {|f| f.gets}
      if first_line =~ /^\s*#.*\bruby\b/i
        begin
          bundle_loader.load!(config_file)
        rescue Exception => e
          raise Webgen::Error.new("Couldn't load webgen configuration file (using Ruby syntax):\n#{e.message}")
        end
      else
        unknown_options = @config.load_from_file(config_file)
        @logger.vinfo { "Configuration data loaded from <#{config_file}>" }
        if unknown_options.length > 0
          @logger.debug { "Ignored following unknown options in configuration file: #{unknown_options.join(', ')}" }
        end
      end
    end
    private :load_configuration

    # Restore the cache using the +website.cache+ configuration option.
    def restore_cache
      @cache = Cache.new
      data = if config['website.cache'].first == 'file'
               File.binread(cache_file) if File.file?(cache_file)
             else
               config['website.cache'].last
             end
      cache_data, version = Marshal.load(data) rescue nil
      @cache.restore(cache_data) if cache_data && version == Webgen::VERSION
    end
    private :restore_cache

    # Save the +cache+.
    def save_cache
      return if config['website.dry_run']
      cache_data = [@cache.dump, Webgen::VERSION]
      if config['website.cache'].first == 'file'
        File.open(cache_file(true), 'wb') {|f| Marshal.dump(cache_data, f)}
      else
        config['website.cache'][1] = Marshal.dump(cache_data)
      end
    end

    # The full path of the cache filename.
    def cache_file(create_dir = false)
      tmpdir(config['website.cache'].last, create_dir)
    end
    private :cache_file

    # Append the path to the website's temporary directory and return the full path to it.
    #
    # Note that the temporary directory is only created if the +create+ parameter is set to true.
    def tmpdir(path = '', create = false)
      @_tmpdir = File.absolute_path(config['website.tmpdir'], @directory) unless defined?(@_tmpdir)
      FileUtils.mkdir_p(@_tmpdir) if create
      File.join(@_tmpdir, path)
    end

    # Execute the given task.
    #
    # See Webgen::Task and the classes in its namespace for available classes.
    def execute_task(task, *options)
      @ext.task.execute(task, *options)
    end

  end

end
