module TCR
  class Cassette
    attr_reader :name

    def self.build(name)
      if cassette_exists?(name)
        RecordedCassette.new(name)
      else
        RecordingCassette.new(name)
      end
    end

    def self.filename(name)
      "#{TCR.configuration.cassette_library_dir}/#{name}.json"
    end

    def self.cassette_exists?(name)
      File.exists?(filename(name))
    end

    def initialize(name)
      @name = name
    end

    protected

    def filename
      self.class.filename(name)
    end

    class RecordingCassette < Cassette
      attr_reader :originally_recorded_at

      def initialize(*args)
        super
        @originally_recorded_at = Time.now
      end

      def sessions
        @sessions ||= []
      end

      def next_session
        Session.new.tap do |session|
          sessions << session
        end
      end

      def finish
        FileUtils.mkdir_p(File.dirname(filename))
        File.open(filename, "w") { |f| f.write(serialized_form) }
      end

      private

      def serialized_form
        raw = {
          "originally_recorded_at" => originally_recorded_at,
          'sessions' => sessions.map(&:as_json)
        }
        JSON.pretty_generate(raw)
      end

      def empty?
        true
      end

      class Session
        def initialize
          @recording = []
        end

        def connect(&block)
          next_command('connect', ret: nil, &block)
        end

        def close(&block)
          next_command('close', &block)
        end

        def read(*args, &block)
          next_command('read', data: args, &block)
        end

        def write(str, &block)
          next_command('write', data: str, &block)
        end

        def as_json
          {"recording" => @recording}
        end

        private

        def next_command(command, options={}, &block)
          yield.tap do |return_value|
            return_value = options[:ret] if options.has_key?(:ret)
            @recording << [command, return_value] + Array(options[:data])
          end
        end
      end
    end

    class RecordedCassette < Cassette
      def sessions
        @sessions ||= serialized_form['sessions'].map{|raw| Session.new(raw)}
      end

      def originally_recorded_at
        serialized_form['originally_recorded_at']
      end

      def next_session
        session = sessions.shift
        raise NoMoreSessionsError unless session
        session
      end

      def finish
        # no-op
      end

      def empty?
        sessions.all?(&:empty?)
      end

      private

      def serialized_form
        @serialized_form ||= begin
          raw = File.open(filename) { |f| f.read }
          JSON.parse(raw)
        end
      end

      class Session
        def initialize(raw)
          @recording = raw["recording"]
        end

        def connect
          next_command('connect')
        end

        def close
          next_command('close')
        end

        def read(*args)
          next_command('read') do |str, *data|
            raise TCR::DataMismatchError.new("Expected to read to be called with args '#{args.inspect}' but was called with '#{data.inspect}'") unless args == data
          end
        end

        def write(str)
          next_command('write') do |len, data|
            raise TCR::DataMismatchError.new("Expected to write '#{str}' but next data in recording was '#{data}'") unless str == data
          end
        end

        def empty?
          @recording.empty?
        end

        private

        def next_command(expected)
          command, return_value, data = @recording.shift
          raise TCR::CommandMismatchError.new("Expected to '#{expected}' but next in recording was '#{command}'") unless expected == command
          yield return_value, *data if block_given?
          return_value
        end
      end
    end
  end
end
