require 'spontaneous/data_mapper/content_model/column_accessors'
require 'spontaneous/data_mapper/content_model/serialization'
require 'spontaneous/data_mapper/content_model/timestamps'
require 'spontaneous/data_mapper/content_model/instance_hooks'
require 'spontaneous/data_mapper/content_model/associations'

module Spontaneous
  module DataMapper
    module ContentModel
      def self.extended(model)
        model.send :include, InstanceMethods
      end

      def self.generate(mapper, &block)
        Class.new do
          extend Spontaneous::DataMapper::ContentModel
          class_eval &block if block
          self.mapper = mapper
          include ColumnAccessors
          include Serialization
          include Timestamps
          include InstanceHooks
          include Associations::InstanceMethods
        end
      end

      attr_reader :mapper
      attr_writer :options

      def inherited(subclass)
        @mapper.inherited(self, subclass)
        subclass.mapper  = @mapper
        super
      end

      def mapper=(mapper)
        @mapper = mapper
      end

      def restricted_columns
        [:id, :type_sid]
      end

      def serialize_columns(*columns)
        define_singleton_method(:serialized_columns) { columns }
      end

      def serialized_columns
        []
      end

      def include_all_types
        include_types()
      end

      def include_types(*types, &block)
        if block_given?
          define_singleton_method(:types, &block)
        else
          define_singleton_method(:types) { types }
        end
      end

      def types
        [self]
      end

      def subclasses
        mapper.subclasses(self)
      end

      def columns
        @columns ||= mapper.columns - restricted_columns
      end

      def db
        mapper.db
      end

      def schema
        mapper.schema
      end

      def get(id)
        mapper.get(id)
      end

      alias_method :[], :get

      def count
        mapper.count(types)
      end

      def all
        mapper.all(*types)
      end

      def all!(&block)
        mapper.all!(&block)
      end

      def first(*args, &block)
        mapper.first(types, *args, &block)
      end

      def first!(*args, &block)
        mapper.first!(*args, &block)
      end

      def filter(*cond, &block)
        mapper.filter(types, *cond, &block)
      end

      def filter!(*cond, &block)
        mapper.filter!(*cond, &block)
      end

      def where(*cond, &block)
        mapper.where(types, *cond, &block)
      end

      def where!(*cond, &block)
        mapper.where!(*cond, &block)
      end

      def create(attrs = {})
        self.new(attrs).save
      end

      def insert(attrs = {})
        mapper.insert(attrs)
      end

      def update(attrs)
        mapper.update(types, attrs)
      end

      def delete(&block)
        mapper.delete(types, &block)
      end

      def order(*columns, &block)
        mapper.order(types, *columns, &block)
      end

      def limit(l, o = (no_offset = true; nil))
        mapper.limit(types, l, o)
      end

      def for_update
        mapper.for_update
      end

      def select(*columns, &block)
        mapper.select(types, *columns, &block)
      end

      def visible
        mapper.visible.dataset(types)
      end

      def columns
        mapper.columns
      end

      def primary_key
        mapper.primary_key
      end

      def table_name
        mapper.table_name
      end

      include Associations

      module InstanceMethods

        HOOKS = [ :create, :save, :update, :destroy ] unless defined?(HOOKS)

        HOOKS.each do |hook|
          module_eval "def before_#{hook}; end", __FILE__, __LINE__
          module_eval "def after_#{hook} ; end", __FILE__, __LINE__
        end

        def initialize(attr = {}, from_db = false)
          @modified = false
          if from_db
            @new = false
            set_attributes!(attr)
          else
            @new = true
            @modified = true
            @attributes = {}
            changed_columns.clear
            set(attr)
          end
          trigger_hook(:after_initialize)
        end

        def after_initialize; end

        def attributes
          @attributes.dup
        end

        def mapper
          model.mapper
        end

        def save
          id = nil
          saved = false
          trigger_hook(:before_save) do
            if new?
              trigger_hook(:before_create) do
                mapper.create(self)
                @new  = false
                saved = true
                trigger_hook(:after_create)
                self
              end
            else
              trigger_hook(:before_update) do
                mapper.save(self)
                saved = true
                changed_columns.clear
                trigger_hook(:after_update)
                self
              end
            end
            return nil unless saved
            trigger_hook(:after_save)
          end
          @modified = false
          self
        end

        def trigger_hook(callback)
          catch :halt do
            send(callback)
            return yield if block_given?
            return self
          end
          nil
        end

        def mark_modified!(*columns)
          if columns.empty?
            @modified = true
          else
            _mark_columns_as_modified(*columns)
          end
        end

        def modified?
          @modified || !changed_columns.empty?
        end

        def reload
          refresh
        end

        def _after_save_refresh
          refresh
        end

        def refresh
          changed_columns.clear
          mapper.reload(self)
          self
        end

        def destroy
          deleted = false
          trigger_hook(:before_destroy) do
            delete
            deleted = true
            trigger_hook(:after_destroy)
            self
          end
          return nil unless deleted
          self
        end

        def delete
          mapper.delete_instance(self)
        end

        def new?
          @new
        end

        def id
          @attributes[:id]
        end

        alias_method :pk, :id

        def update(values)
          set(values)
          save
        end

        def set(values)
          set_restricted(values)
        end

        def model
          self.class
        end

        def set_restricted(attrs)
          exclude = model.restricted_columns
          attrs.each do |column, value|
            send("#{column}=", value) unless exclude.include?(column.to_sym)
          end
        end

        def set_attributes!(attributes)
          @attributes = attributes
        end

        def set_attributes_after_save!(attributes)
          set_attributes!(attributes)
          changed_columns.clear
          @new = false
        end

        def ==(obj)
          eql?(obj)
        end

        def eql?(obj)
          (obj.class == model) && (obj.attributes == @attributes)
        end

        def hash
          [model, id.nil? ? @values.sort_by{|k,v| k.to_s} : id].hash
        end
      end
    end
  end
end
