# -*- coding: UTF-8 -*-

require 'mj/tools/ssh'

require 'active_record'
require 'fileutils'

module BuildTool

    #
    # Represents the information associated with a buildable module.
    #
    class Module < ActiveRecord::Base

        # The name of the module
        validates_uniqueness_of :name

        # Modules are sorted by name
        def <=>( other )
            self.name <=> other.name
        end

        # A command can have many module events
        has_many :module_logs, :class_name => "BuildTool::History::ModuleLog", :order => :id, :foreign_key => :module, :primary_key => :name, :dependent => :destroy

        # Custom initialization
        after_initialize :my_initialize
        def my_initialize()
            if name.nil?
                raise StandardError, "Module name is required!"
            end
            @active = nil
            @patches = Array.new
            @local_path = nil
            @build_prefix = nil
            @remote_path = nil
            @environment = nil
            @build_system = nil
            @install_prefix = nil
            @is_template = false
            @vcs_configuration = nil
            @feature = nil
            @default_active = true
            @parent = nil

            @long_description = nil
            @description = nil
            @last_success = nil
        end

        #
        ### ATTRIBUTES
        #

        attr_accessor :feature
        attr_reader :patches

        # The previous version of this module.
        attr_accessor :parent

        # The default state of the feature
        attr_accessor :default_active

        # Is the module active by default?
        def default_active?
            @default_active
        end

        # Is the module active?
        def active?
            # If the module is explicitely de/activated that wins
            if active.nil?
                # if the module is active by default let the feature decide if present.
                if default_active?
                    if @feature.nil?
                        return true
                    else
                        return @feature.active?
                    end
                else
                    # The feature is not active by default.
                    return false
                end
            else
                return active
            end
        end

        # not inherited
        def build_directory
            build_prefix_required.join("bld", local_path)
        end

        def build_prefix=( path )
            if path
                path = Pathname.new( path.sub( /\$HOME/, '~' ) )
                if path.to_s[0] != '~' and path.relative?
                    raise ConfigurationError, "Build-prefix '#{path}' is relative!"
                end
                @build_prefix = path.expand_path
            else
                @build_prefix = nil
            end
        end

        def build_prefix
            # Return our own buildsystem if there is one
            return @build_prefix if @build_prefix
            # Return our parents buildsystem if there is one
            return parent.build_prefix if parent && parent.build_prefix
            # Nothing
            nil
        end

        # Will throw a exception if build_prefix is not set
        def build_prefix_required
            return self.build_prefix if self.build_prefix
            raise ConfigurationError, "No build prefix configured for #{name}!"
        end

        # Build system
        attr_writer :build_system

        def build_system
            # Return our own buildsystem if there is one
            return @build_system if @build_system
            # Return our parents buildsystem if there is one
            if parent && parent.build_system
                @build_system = parent.build_system.dup
                @build_system.module = self
                return @build_system
            end
            # Nothing
            nil
        end

        def our_build_system
            @build_system
        end

        # Will throw a exception if build_system is not set
        def build_system_required
            return self.build_system if self.build_system
            # *TODO* try to guess the build system
            raise ConfigurationError, "No build system configured for #{name}!"
        end

        # Return true if the last build failed for whatever reason
        def broken?
            return true if lastlog.empty?
            lastlog[0].module_logs.where( :module => name ).each do |e|
                if e.state != History::ModuleLog::FINISHED_SUCCESSFUL
                    return true
                end
            end
            return false
        end

        def checkedout?
            vcs_required.checkedout?
        end

        def configured?
            if build_system
                return build_system.configured?
            else
                return false
            end
        end

        attr_writer :description
        def description
            @description
        end

        # Environment
        attr_writer :environment

        def environment
            # Return our own buildsystem if there is one
            return @environment if @environment
            # Return our parents buildsystem if there is one
            return parent.environment if parent && parent.environment
            # Nothing
            nil
        end

        # Will throw a exception if environment is not set
        def environment_required
            return self.environment if self.environment
            raise ConfigurationError, "No environment configured for #{name}!"
        end

        # Garbage collect
        def gc
            vcs.gc
        end

        # Installation prefix
        def install_prefix=( path )
            if path
                path = Pathname.new( path.sub( /\$HOME/, '~' ) )
                if path.to_s[0] != '~' and path.relative?
                    raise ConfigurationError, "Install-prefix '#{path}' is relative!"
                end
                @install_prefix = path.expand_path
            else
                @install_prefix = nil
            end
        end

        def install_prefix
            # Return our own buildsystem if there is one
            return @install_prefix if @install_prefix
            # Return our parents buildsystem if there is one
            return parent.install_prefix if parent && parent.install_prefix
            # Nothing
            nil
        end

        # Will throw a exception if install_prefix is not set
        def install_prefix_required
            return self.install_prefix if self.install_prefix
            raise ConfigurationError, "No install prefix configured for #{name}!"
        end

        attr_writer :is_template
        def is_template?
            @is_template
        end

        def local_path=( local_path )
            raise ConfigurationError, "Attempt to set local_path for module template #{name}" if is_template?
            @local_path = local_path
        end

        def local_path
            @local_path || name
        end

        attr_writer :long_description
        def long_description
            @long_description
        end

        # Remote path
        def remote_path=( remote_path )
            raise ConfigurationError, "Attempt to set remote_path for module template #{name}" if is_template?
            @remote_path = remote_path
        end

        def remote_path
            @remote_path || name
        end

        def source_directory
            build_prefix_required.join("src", local_path)
        end
        alias source_directory_required source_directory

        # Return the logfile of the last compilation attempt
        def lastlog
            return BuildTool::History::CommandLog.last_by_module( name )
        end

        # Returns a current state in string format
        def state
            return 'UNKNOWN' if lastlog.empty?
            lastlog[0].module_logs.where( :module => name ).each do |e|
                if e.state != History::ModuleLog::FINISHED_SUCCESSFUL
                    return "#{e.state_str} (#{e.event})"
                end
            end
            return History::ModuleLog::state_str( History::ModuleLog::FINISHED_SUCCESSFUL )
        end

        # Return the current state as one char.
        def state_char
            return '?' if lastlog.empty?
            lastlog[0].module_logs.where( :module => name ).each do |e|
                if e.state != History::ModuleLog::FINISHED_SUCCESSFUL
                    return "#{e.state_char}"
                end
            end
            return History::ModuleLog::state_char( History::ModuleLog::FINISHED_SUCCESSFUL )
        end

        def last_success
            return @last_success if @last_success
            lastlog = BuildTool::History::CommandLog.last_success_by_module( name )

            if not lastlog.nil?
                @last_success = lastlog[:finished_at]
            end

            if @last_success.nil?
                @last_success = DateTime.new
            end

            return @last_success
        end

        def active_char
            if active?
                ANSI::Code.green { "A" }
            else
                "I"
            end
        end

        def vcs
            return vcs_configuration.vcs( self ) if vcs_configuration
            nil
        end

        attr_writer :vcs_configuration
        def vcs_configuration
            return @vcs_configuration if @vcs_configuration
            if parent && parent.vcs_configuration
                # puts "copying vcs for #{name} from #{parent.name}"
                vc = parent.vcs_configuration.class.new
                vc.copy_configuration( parent.vcs_configuration )
                @vcs_configuration = vc
                return vc
            end
            nil
        end

        def vcs_configuration_required
            vc = vcs_configuration
            if vc.nil?
                raise ConfigurationError, "No version control system configure for module #{name}."
            end
            return vc
        end

        def vcs_required
            if source_directory.nil?
                raise ConfigurationError, "No source directory specified for module #{name}."
            end
            return vcs_configuration_required.vcs( self )
        end

        #
        ### ACTIONS
        #
        def clean
            build_system_required.make( "clean" )
        end

        def remove_build_directory
            build_system_required.remove_build_directory
        end

        def remove_source_directory
            build_system_required.remove_source_directory
        end

        # Clone the repository.
        def clone
            vcs_required.clone
            if !patches.empty?
                if !vcs.patches_supported?
                    raise NotImplementedError, "Patch support not implemented for vcs #{vcs.name}"
                end
                vcs_required.apply_patches( patches )
            end
        end

        # Fetch changes from the remote repository. Do not change the local
        # checkout.
        def fetch( verbose = false )
            vcs_required.fetch( verbose = verbose )
        end

        # Update the local changes with remote changes. Do not fetch changes
        # from the remote repository.
        def rebase( verbose )
            vcs_required.rebase( verbose )
            if !patches.empty?
                if !vcs.patches_supported?
                    raise NotImplementedError, "Patch support not implemented for vcs #{vcs.name}"
                end
                vcs_required.apply_patches( patches )
            end
            build_system_required.after_rebase
        end

        def configure
            build_system_required.configure
        end

        def reconfigure
            build_system_required.reconfigure
        end

        # Call make
        def make( target = nil )
            build_system_required.make( target )
        end

        def install( fast = false )
            build_system_required.install( fast )
        end

        # Cleanup after vcs_access
        def cleanup_after_vcs_access
            if MJ::Tools::SSH::has_keys?
                logger.info ""
                logger.info "#### Cleaning up the ssh keys"
                MJ::Tools::SSH::keys.each do |file|
                    logger.info "  - Removing key #{file}"
                end
                MJ::Tools::SSH::cleanup()
            end
        end

        # Check if an ssh-key is required and active it if necessary
        def prepare_for_fetch
            if vcs
                return vcs.prepare_for_fetch
            end
            true
        end

        # Check if an ssh-key is required and active it if necessary
        def prepare_for_rebase
            if vcs
                return vcs.prepare_for_rebase
            end
            true
        end

        # Check if an ssh-key is required and active it if necessary
        def ready_for_fetch
            if vcs
                return vcs.ready_for_fetch
            end
            true
        end

        def ready_for_rebase
            if vcs
                return vcs.ready_for_rebase
            end
            true
        end


        def prepare_for_installation
            return build_system_required.prepare_for_installation
        end

        def shell( command = nil, options = {} )

            envvars = options[ :envvars ] || {}
            options[ :envvars ] = envvars.merge( {
                'BT_SOURCE' => source_directory.to_s
            } )

            environment.shell( command, options )
        end

        def to_s
            "#{object_id}: #{name}"
        end

    end # Module


end # module BuildTool