# -*- coding: utf-8 -*-
#
# CFTypes, e.g. CFString, CFInteger
# needed to create unambiguous plists
#
# Author::    Christian Kruse (mailto:cjk@wwwtech.de)
# Copyright:: Copyright (c) 2009
# License::   MIT License

require 'base64'

module Facter::Util::CFPropertyList
  # This class defines the base class for all CFType classes
  #
  class CFType
    # value of the type
    attr_accessor :value

    def initialize(value=nil)
      @value = value
    end

    def to_xml(parser)
    end

    def to_binary(bplist) end
  end

  # This class holds string values, both, UTF-8 and UTF-16BE
  # It will convert the value to UTF-16BE if necessary (i.e. if non-ascii char contained)
  class CFString < CFType
    # convert to XML
    def to_xml(parser)
      n = parser.new_node('string')
      n = parser.append_node(n, parser.new_text(@value)) unless @value.nil?
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.string_to_binary(@value);
    end
  end

  # This class holds integer/fixnum values
  class CFInteger < CFType
    # convert to XML
    def to_xml(parser)
      n = parser.new_node('integer')
      n = parser.append_node(n, parser.new_text(@value.to_s))
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.num_to_binary(self)
    end
  end

  # This class holds float values
  class CFReal < CFType
    # convert to XML
    def to_xml(parser)
      n = parser.new_node('real')
      n = parser.append_node(n, parser.new_text(@value.to_s))
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.num_to_binary(self)
    end
  end

  # This class holds Time values. While Apple uses seconds since 2001,
  # the rest of the world uses seconds since 1970. So if you access value
  # directly, you get the Time class. If you access via get_value you either
  # geht the timestamp or the Apple timestamp
  class CFDate < CFType
    TIMESTAMP_APPLE = 0
    TIMESTAMP_UNIX  = 1;
    DATE_DIFF_APPLE_UNIX = 978307200

    # create a XML date strimg from a time object
    def CFDate.date_string(val)
      # 2009-05-13T20:23:43Z
      val.getutc.strftime("%Y-%m-%dT%H:%M:%SZ")
    end

    # parse a XML date string
    def CFDate.parse_date(val)
      # 2009-05-13T20:23:43Z
      val =~ %r{^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$}
      year,month,day,hour,min,sec = $1, $2, $3, $4, $5, $6
      return Time.utc(year,month,day,hour,min,sec).getlocal
    end

    # set value to defined state
    def initialize(value = nil,format=CFDate::TIMESTAMP_UNIX)
      if(value.is_a?(Time) || value.nil?) then
        @value = value.nil? ? Time.now : value
      elsif value.instance_of? Date
        @value = Time.utc(value.year, value.month, value.day, 0, 0, 0)
      elsif value.instance_of? DateTime
        @value = value.to_time.utc
      else
        set_value(value,format)
      end
    end

    # set value with timestamp, either Apple or UNIX
    def set_value(value,format=CFDate::TIMESTAMP_UNIX)
      if(format == CFDate::TIMESTAMP_UNIX) then
        @value = Time.at(value)
      else
        @value = Time.at(value + CFDate::DATE_DIFF_APPLE_UNIX)
      end
    end

    # get timestamp, either UNIX or Apple timestamp
    def get_value(format=CFDate::TIMESTAMP_UNIX)
      if(format == CFDate::TIMESTAMP_UNIX) then
        @value.to_i
      else
        @value.to_f - CFDate::DATE_DIFF_APPLE_UNIX
      end
    end

    # convert to XML
    def to_xml(parser)
      n = parser.new_node('date')
      n = parser.append_node(n, parser.new_text(CFDate::date_string(@value)))
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.date_to_binary(@value)
    end
  end

  # This class contains a boolean value
  class CFBoolean < CFType
    # convert to XML
    def to_xml(parser)
      parser.new_node(@value ? 'true' : 'false')
    end

    # convert to binary
    def to_binary(bplist)
      bplist.bool_to_binary(@value);
    end
  end

  # This class contains binary data values
  class CFData < CFType
    # Base64 encoded data
    DATA_BASE64 = 0
    # Raw data
    DATA_RAW = 1

    # set value to defined state, either base64 encoded or raw
    def initialize(value=nil,format=DATA_BASE64)
      if(format == DATA_RAW)
        @raw_value = value
        @raw_value.blob = true
      else
        @value = value
      end
    end

    # get base64 encoded value
    def encoded_value
      @value ||= "\n#{Base64.encode64(@raw_value).gsub("\n", '').scan(/.{1,76}/).join("\n")}\n"
    end

    # get base64 decoded value
    def decoded_value
      @raw_value ||= String.new(Base64.decode64(@value))
      @raw_value.blob = true
      @raw_value
    end

    # convert to XML
    def to_xml(parser)
      n = parser.new_node('data')
      n = parser.append_node(n, parser.new_text(encoded_value()))
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.data_to_binary(decoded_value())
    end
  end

  # This class contains an array of values
  class CFArray < CFType
    # create a new array CFType
    def initialize(val=[])
      @value = val
    end

    # convert to XML
    def to_xml(parser)
      n = parser.new_node('array')
      @value.each do |v|
        n = parser.append_node(n, v.to_xml(parser))
      end
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.array_to_binary(self)
    end
  end

  # this class contains a hash of values
  class CFDictionary < CFType
    # Create new CFDictonary type.
    def initialize(value={})
      @value = value
    end

    # convert to XML
    def to_xml(parser)
      n = parser.new_node('dict')
      @value.each_pair do |key, value|
        k = parser.append_node(parser.new_node('key'), parser.new_text(key.to_s))
        n = parser.append_node(n, k)
        n = parser.append_node(n, value.to_xml(parser))
      end
      n
    end

    # convert to binary
    def to_binary(bplist)
      bplist.dict_to_binary(self)
    end
  end
end

# eof
