require 'ostruct'
require 'date'
require 'time'

# Class representing blog page, as at http://thinkinghard.com/blog
class BlogPage < Regenerate::PageObject
    
  # Regex to parse date of form '23 May, 2014'
  DATE_REGEX = /[0-9]+[ ]+[A-Za-z]+,[ ]+([0-9]+)/

  # Process the blog page object. 
  # Required instance variables to be set before hand are @title, @dateString, @blurb
  # Instance variables that get updated are @header, @footer
  def process
    dateMatch = DATE_REGEX.match(@dateString) # read the date
    if dateMatch
      # Pull out the year to put into a copyright notice
      @year = dateMatch[1].to_i
      # puts " parsed @year = #{@year} from #{@dateString.inspect}"
    else
      raise Exception.new("Invalid date string: #{@dateString.inspect}")
    end
    # List of previous stories that is referenced in the header template
    @previousStories = BlogEntries.getEntries(relative_path("_entries.txt")).getPreviousEntries(@baseFileName, 10)
    
    # Generate header and footer
    @header=erb("_templates/_header.html")
    @footer=erb("_templates/_footer.html")
    
    # Save properties for this blog page (so that its details can be referenced when generating other pages, 
    # without having to re-read the source for this page)
    saveProperties
  end
  
  # List of page object instance variables to save into the properties file for this blog page
  def propertiesToSave
    [:dateString, :year, :tags, :title, :blurb, :baseFileName]
  end
  
  # Create properties file name as a function of the blog page source file name
  # (The directory is prefixed with '_' on the assumption that files and directories with this prefix
  #  will not get uploaded to the final website.)
  def self.propertiesFileName(fileName)
    File.join("_properties", fileName + ".properties")
  end    
end

# A list of information about all the blog pages
# The list is defined by an "entries" text file that lists each blog source file
# on one line, in order of date (i.e. new entries added to the bottom)
class BlogEntries
  
  # A cached mapping from "entries file" name to BlogEntries
  ENTRIES = {}
  
  # Get the possibly cached entries information for a given entries file name
  def self.getEntries(entriesFile)
    entries = ENTRIES[entriesFile]
    if entries == nil
      entries = BlogEntries.new(entriesFile)
      ENTRIES[entriesFile] = entries
    end
    entries
  end
  
  # Initialise the properties for a given entries file:
  # * Read the entries file
  # * For each entry, read the saved properties for each file
  # * Reverse the order (so newest entries come first)
  def initialize(entriesFile)
    puts "Reading BlogEntries from #{entriesFile} ..."
    @entriesFile = entriesFile
    @baseDir = File.dirname(@entriesFile)
    @entryProperties = {}
    @entryFileNames = []
    for line in File.readlines(entriesFile) do
      line = line.chomp
      if line != ""
        entryFileName = line
        @entryFileNames << entryFileName
        readEntryProperties(entryFileName)
      end
    end
    @entryFileNames.reverse!
  end
  
  # Read entry properties for one blog page:
  # * Determine the properties file name for that blog page
  # * If that file exists, read the properties (which are in JSON format), and put into @entryProperties
  def readEntryProperties(entryFileName)
    propertiesFileName = File.join(@baseDir, BlogPage.propertiesFileName(entryFileName))
    if File.exist? propertiesFileName
      properties = OpenStruct.new(JSON.parse(File.read(propertiesFileName)))
      @entryProperties[entryFileName] = properties
    end
  end
  
  # Convert a list of entry names to a list of properties, leaving out those with no properties
  def entryNamesToProperties(entryNames)
    entryNames.map { |entryFileName| @entryProperties[entryFileName] } .compact
  end
  
  # Get all the entries as properties
  def getEntriesAsProperties
    entryNamesToProperties(@entryFileNames)
  end
  
  # Get a certain number of entries previous to a specified blog entry as properties
  def getPreviousEntries(entryFileName, numEntries)
    entryPos = @entryFileNames.index(entryFileName)
    if entryPos == nil
      []
    else
      previousEntryNames = @entryFileNames[entryPos+1..entryPos+numEntries]
      entryNamesToProperties(previousEntryNames)
    end
  end
  
end

# Page object for the blog index page, at http://thinkinghard.com/blog/index.html
class BlogIndexPage < Regenerate::PageObject
  
  # Process by reading the list of blog entries, and updating @body using the specified
  # template (where the template is defined in-line in the source file)
  def process(templateString)
    @blogEntries = BlogEntries.getEntries(relative_path("_entries.txt"))
    @entries = @blogEntries.getEntriesAsProperties
    @body=erbFromString(templateString)
  end
end

# Page object for the blog RSS page at http://thinkinghard.com/blog/rss.xml
class BlogRssPage < Regenerate::PageObject
  attr_reader :baseUrl
  
  # Construct properties required for one RSS item from the properties for one blog entry
  def rssItemFromProperties(properties)
    pubDate = Date.parse(properties.dateString).to_time
    OpenStruct.new({ :titleXmlEncoded => properties.title.gsub("&ndash;", "&#8211;"), 
                     :url => @baseUrl + properties.baseFileName, 
                     :blurb => properties.blurb, 
                     :pubDate => pubDate, :pubDateFormatted => pubDate.utc.iso8601 })
  end

  # Last build date is the last date any item was published, or (by default) now
  def findLastBuildDate(items)
    lastDate = nil
    for item in items do
      itemDate = item.pubDate
      if lastDate == nil || itemDate > lastDate
        lastDate = itemDate
      end
    end
    if lastDate == nil
      lastDate = Time.now
    end
    return lastDate
  end
  
  # Convenience function to write XML cdata
  def cdata(string)
    "<![CDATA[#{string}]]>"
  end

  # Process by:
  # * reading all blog entry properties
  # * converting to RSS item properties
  # * calculating last build date
  # * updating @body from specified template (which is defined in-line in the source file)
  def process(templateString)
    @blogEntries = BlogEntries.getEntries(relative_path("_entries.txt"))
    @entries = @blogEntries.getEntriesAsProperties
    @items = @entries.map{|properties| rssItemFromProperties(properties)}.reverse
    @lastBuildDate = findLastBuildDate(@items).utc.iso8601
    @body=erbFromString(templateString)
  end
end
  
