/////////////////////////////////////////////////////////////////////////////////
//Copyright (c) 2009  Andrew L. Sandoval
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
/////////////////////////////////////////////////////////////////////////////////
//
//
// XBMtoDWS - Convert xbm image file to DigitalWindowSticker file
//			  For use with the Digital Window Sticker device
//			  A link to the article with the Digital Window Sticker project can be found at:
//			  http://www.netwaysglobal.com/DWS/
//
// Written by Andrew L. Sandoval, October 2009
//
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <iterator>
#include <algorithm>
#include <cctype>

//
// Spirit for parsing:
//
#include <boost/spirit.hpp>
#include <boost/spirit/phoenix/binders.hpp>
#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>
#include <boost/spirit/include/classic_core.hpp>
#include <boost/spirit/include/classic_push_back_actor.hpp>
#include <boost/spirit/actor.hpp>
#include <boost/spirit/utility.hpp>
#include <boost/spirit/symbols.hpp>
#include <boost/spirit/dynamic/if.hpp>

#include <boost/dynamic_bitset.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/bind.hpp>

struct XBMParser : public boost::spirit::grammar<XBMParser>
{
	struct hex_closure : boost::spirit::closure<hex_closure, unsigned char>
	{
		member1 m_ucValue;
	};

	struct string_closure : boost::spirit::closure<string_closure, std::string>
	{
		member1 m_strName;
	};

	template<typename ScannerT>
	struct definition
	{
		definition(XBMParser const &self) 
		{
			using namespace boost::spirit;
			using namespace phoenix;

			m_litPoundDefine = str_p("#define");
			m_litBits = str_p("_bits");
			m_litStatic = str_p("static");
			m_litUnsigned = str_p("unsigned");
			m_litChar = str_p("char");

			m_identchar = alnum_p | ch_p('_') | ch_p('?') | ch_p('@') | ch_p('$') | ch_p(':') | ch_p('`');
		
			// Normally (for C/C++) we would start with alpha_p, but since XBM files can be called
			// 1.xbm with 1 as the identifier name, we support alnum_p here for initial character
			// in the identifier:
			m_identifier = lexeme_d[ (alnum_p | '_') >> 
				*m_identchar ][m_identifier.m_strName = construct_<std::string>(arg1, arg2)];

			m_defineLeft = (m_identifier[m_defineLeft.m_strName = arg1] >> +space_p)
				[bind(&XBMParser::SetDefineLeft)(self, m_defineLeft.m_strName)];

			m_defineRight = ((m_identifier[m_defineRight.m_strName = arg1] |
				(!str_p("0x") >> *digit_p)[m_defineRight.m_strName = construct_<std::string>(arg1, arg2)])
				>> *blank_p >> eol_p)
				[bind(&XBMParser::SetDefineRight)(self, m_defineRight.m_strName)];

			m_define = (*space_p >> m_litPoundDefine >> +space_p >> m_defineLeft >> m_defineRight)
				[bind(&XBMParser::SetDefine)(self, construct_<std::string>(arg1, arg2))];

			m_startData = !m_litStatic >> +space_p >> m_litUnsigned >> +space_p >>
				m_litChar >> +space_p >> m_identifier >> ch_p('[') >> *space_p >> ch_p(']') >>
				*space_p >> ch_p('=') >> *space_p >> ch_p('{') >> *space_p;

			m_endData = *space_p >> ch_p('}') >> *space_p >> ch_p(';');

			m_data = *space_p >> 
				( (str_p("0x") >> hex_p[m_data.m_ucValue = construct_<unsigned char>(arg1)]) |
				(uint_p[m_data.m_ucValue = construct_<unsigned char>(arg1)]) )
				[bind(&XBMParser::AppendBits)(self, m_data.m_ucValue)];

			m_cComment = comment_p("/*", "*/");

			m_cppComment = comment_p("//");

			m_comments = *space_p >> (m_cComment | m_cppComment) >> *space_p;

			m_dataLines = m_startData >> *m_comments >> *space_p >>
				(m_data % ch_p(',')) >> m_endData;

			m_statements = m_define | m_comments | m_dataLines;

			m_input = lexeme_d
			[
				*space_p >>  *m_statements >> *space_p
			];
		}
		boost::spirit::rule<ScannerT> m_litPoundDefine, m_litBits, m_litStatic;
		boost::spirit::rule<ScannerT> m_litUnsigned, m_litChar, m_identchar;
		boost::spirit::rule<ScannerT> m_startData, m_endData;
		boost::spirit::rule<ScannerT> m_cComment, m_cppComment, m_comments, m_dataLines;
		boost::spirit::rule<ScannerT> m_statements, m_input;

		boost::spirit::rule<ScannerT, string_closure::context_t> m_identifier, m_define;
		boost::spirit::rule<ScannerT, string_closure::context_t> m_defineLeft, m_defineRight;

		boost::spirit::rule<ScannerT, hex_closure::context_t> m_data;

		boost::spirit::rule<ScannerT> const& start() const
		{
			return m_input;
		}
	};

	XBMParser(boost::dynamic_bitset<unsigned char> &bsPixelsLeftToRight) :
		m_bsPixelsLeftToRight(bsPixelsLeftToRight), m_pStrLeft(new std::string),
			m_pStrRight(new std::string), m_strLeft(*m_pStrLeft), m_strRight(*m_pStrRight)
	{
	}

	void AppendBits(unsigned char ucValue) const
	{
		m_bsPixelsLeftToRight.append(ucValue);
	}

	void SetDefineLeft(const std::string &strName) const
	{
		m_strLeft = strName;
	}

	void SetDefineRight(const std::string &strName) const
	{
		m_strRight = strName;
	}

	unsigned long GetHexOrDecimalValue(const std::string &strValue) const
	{
		unsigned long ulValue = 0;
		char *pcEnd;
		if(strValue.find("0x") == 0)	// Hex
		{
			ulValue = strtoul(strValue.c_str() + 2, &pcEnd, 16);
		}
		else
		{
			ulValue = strtoul(strValue.c_str(), &pcEnd, 10);
		}
		return ulValue;
	}

	void SetDefine(const std::string &strName) const
	{
		//std::cout << strName << ": " << m_strLeft << " = " << m_strRight << std::endl;
		if(m_strLeft.find("_width") != m_strLeft.npos)
		{
			// Assume this is the width definition, could be screwed-up by #define _width_fred_height something
			// Also only handle numeric values, this could be expanded to build a symbol table if needed
			unsigned long ulWidth = GetHexOrDecimalValue(m_strRight);
			if(ulWidth != 48)
			{
				std::cerr << "Width of " << ulWidth << " is not supported!" <<
					std::endl << "Digital Window Sticker requires a width of 48 pixels!" << std::endl;
				// cheap shot to exit here, but effective (should really stop the parser)
				exit(1);
			}
			return;
		}
		else if(m_strLeft.find("_height") != m_strLeft.npos)
		{
			unsigned long ulHeight = GetHexOrDecimalValue(m_strRight);
			if(ulHeight != 16)
			{
				std::cerr << "Height of " << ulHeight << " is not supported!" <<
					std::endl << "Digital Window Sticker requires a height of 16 pixels!" << std::endl;
				exit(1);
			}
			return;
		}
		std::cerr << "Ignoring unknown: " << strName << std::endl;
	}

	boost::dynamic_bitset<unsigned char> &m_bsPixelsLeftToRight;
	// Temporaries:
	boost::shared_ptr<std::string> m_pStrLeft;
	std::string &m_strLeft;
	boost::shared_ptr<std::string> m_pStrRight;
	std::string &m_strRight;
};

bool XBMParse(const std::string &fileData, boost::dynamic_bitset<unsigned char> &bsPixelsLeftToRight)
{
	using namespace boost::spirit;

	XBMParser parser(bsPixelsLeftToRight);
	parse_info<> info = parse(fileData.c_str(), parser);
	if(info.full)
	{
		return true;
	}
	else
	{
		std::ostringstream atspot;
		atspot << "Parsing failed at " << info.stop;
		std::cerr << atspot.str() << std::endl;
	}
	return false;
}

void WriteByteToFile(unsigned char ucValue, FILE *f)
{
	fwrite(&ucValue, 1, 1, f);
}

void PrintByte(unsigned char ucValue, int iComma, size_t &szCount, int &iUpCount)
{
	char buf[20];
	std::string strExtra;
	if(--szCount)
	{
		strExtra = ", ";
	}
	if(!(++iUpCount % iComma) || !szCount)
	{
		strExtra += "\n";
		iUpCount = 0;
	}
#ifdef WIN32	// Super lame
	_snprintf(buf, sizeof(buf), "0x%02x%s", ucValue, strExtra.c_str());
#else
	snprintf(buf, sizeof(buf), "0x%02x%s", ucValue, strExtra.c_str());
#endif
	std::cout << buf;
}

int usage(const char *pcProgramName)
{
	std::cerr << "Usage " << pcProgramName << " filename.xbm [-delay nnnn(milliseconds)] [-inverse]" <<
		std::endl << "Digital Window Sticker XBM converter" << std::endl <<
		"Written by Andrew L. Sandoval, for the Digital Window Sticker Circuit" <<
		std::endl << "Default delay is 1000 milliseconds (1 second)" << std::endl;
	return 1;
}

int main(int argc, const char *argv[])
{
	if(argc < 2)
	{
		return usage(argv[0]);
	}
	unsigned long ulDelay = 1000;
	bool bInverse = false;
	for(int i = 2; i < argc; ++i)
	{
		std::string strArg = argv[i];
		if(strArg == "-delay")
		{
			ulDelay = boost::lexical_cast<unsigned long>(argv[i+1]);
			++i;
			std::cout << "Setting delay to " << ulDelay << "ms." << std::endl;
			continue;
		}
		if(strArg == "-inverse")
		{
			bInverse = true;
			continue;
		}
		return usage(argv[0]);	// Unknown Option!
	}

	// Read File
	std::ifstream inputFile(argv[1]);
	if(!inputFile.is_open())
	{
		std::cerr << "Error, can't open file: " << argv[1] << std::endl;
		return 1;
	}
	std::string strFileContents;
	while(!inputFile.eof())
	{
		std::string strLine;
		getline(inputFile, strLine);
		strFileContents += strLine;
		strFileContents += "\n";
	}
	inputFile.close();

	boost::dynamic_bitset<unsigned char> bsLeftToRight;	// XBM bits, left to right
	bool bParsed = XBMParse(strFileContents, bsLeftToRight);
	if(!bParsed)
	{
		std::cerr << "Failed to parse file, make sure it is an .xbm (X-Bitmap) file!" << std::endl;
		return 1;
	}

	if(bsLeftToRight.size() != 768)
	{
		std::cerr << "Error: " << bsLeftToRight.size() << " bytes read.  Expected 768 bytes!" << std::endl;
		return 1;
	}

	//
	// Invert if requested...
	//
	if(bInverse)
	{
		std::cout << "Inverting image..." << std::endl;
		bsLeftToRight.flip();
	}

	//
	// Bits will be arranged left to right 0 to 47, with 16 rows
	// This needs to be converted to a bitset of 0 to 15, with 48 rows
	std::vector<unsigned char> vucImage;
	for(unsigned int row = 0; row < 48; ++row)
	{
		boost::dynamic_bitset<unsigned char> bsRow(16, 0);
		for(unsigned int col = 0; col < 16; ++col)
		{
			bsRow[col] = bsLeftToRight[(col*48)+row];
		}
		for(int i = 0; i < 2; ++i)
		{
			unsigned char ucByte = 0;
			for(unsigned int bit = 0; bit < 8; ++bit)
			{
				if(bsRow[(i*8) + bit])
				{
					ucByte |= (1 << bit);
				}
			}
			vucImage.push_back(ucByte);
		}
	}

	// Going to be lazy & inconsistent, but this is easier than dealing with istream...I think
	std::string strOutputName(argv[1]);
	std::string strLowerName = strOutputName;
	std::transform(strLowerName.begin(), strLowerName.end(), strLowerName.begin(), (int(*)(int))std::tolower);
	std::string::size_type sz = strLowerName.find(".xbm");
	if(sz != std::string::npos)
	{
		strOutputName.erase(sz);
	}
	strOutputName += ".dws";
	FILE *f = fopen(strOutputName.c_str(), "wb");
	std::for_each(vucImage.begin(), vucImage.end(), boost::bind(WriteByteToFile, _1, f));
	//
	// Write Delay:
	//
	fwrite(&ulDelay, sizeof(ulDelay), 1, f);
	fclose(f);

	//
	// Notify User:
	//
	std::cout << "Created file " << strOutputName << " successfully with the following bytes:" << std::endl;
	size_t szCount = vucImage.size(); 
	int iUpCount = 0;
	std::for_each(vucImage.begin(), vucImage.end(), boost::bind(PrintByte, _1, 8, boost::ref(szCount), boost::ref(iUpCount)));
	std::cout << "Delay in ms.: " << ulDelay << std::endl;

	return 0;
}
