#!/usr/bin/env python
""" Generate a cmake file for a c++ project with a target and a test
target 
----------------------

Catch is the C++ Test Framework
Test code are kept in test
Ignores "build" directories
"""

import os
from os import walk
from os import path
from glob import glob
from jinja2 import Template
import sys
import argparse
import urllib2

CATCH_HPP_PATH = "./catch.hpp"

CATCH_URL = "https://raw.github.com/philsquared/Catch/master/single_include/catch.hpp"
CATCH_TEMPLATE = """
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
"""

CMAKE_TEMPLATE_STRING = """
cmake_minimum_required(VERSION 2.8.8)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -Wparentheses -std={{ std }}")
project ( {{target}} )


include_directories(
{%- for includeDir in includeDirs %} 
    "${PROJECT_SOURCE_DIR}/{{includeDir}}"
{%- endfor %}
)

{% if not testonly %}
add_executable( {{ target }}
{%- for file in fileList %}
    "${PROJECT_SOURCE_DIR}/{{file}}"
{%- endfor %}
)
{% endif %}
target_link_libraries (a dl pthread)
{% if needTest %}
enable_testing(true)  # Enables unit-testing.
add_executable( test{{ target }} 
{%- for file in testFileList %}
    "${PROJECT_SOURCE_DIR}/{{file}}"
{%- endfor %}
{%- for file in fileListWithoutMain %}
    "${PROJECT_SOURCE_DIR}/{{file}}"
{%- endfor %}
)

add_test( runUnitTests test{{ target }} )
{% endif %}
"""

FILES_EXTENSIONS_OF_INTEREST = [".c",".cpp",".h"]
CPP_EXT = ".cpp"
C_EXT = ".c"
HEADER_EXTENSIONS = [".hpp",".h"]
BUILD_DIR_STRING = "build"
TEST_DIR_STRING = "test"
TEST_MAIN = "test.cpp"
CPP_STD = ["c++98", "c++03", "c++11", "c++14"]

class CMakeFileMaker:

    def __init__(self):
        pass

    def fileHasExtension(self, filePath, extensions):
        fileName, fileExtension = os.path.splitext(filePath)
        return True if (fileExtension in extensions) else False

    def directoryHasHeaders(self, files):
        return any([self.fileHasExtension(file, HEADER_EXTENSIONS)
                    for file in files])

    def directoryHasTestFile(self, files):
        return True if TEST_MAIN in files else False

    def appendCompileableFiles(self, fileList, files, root):
        for file in files:
            if (file == TEST_MAIN): continue
            if self.fileHasExtension(file,[CPP_EXT, C_EXT]):
                fileList.append(path.join(root.lstrip("./"),file))
        return fileList

    def appendIncludeDirectory(self, includeDirList,files, root):
        if self.directoryHasHeaders(files):
            includeDirList.append(root.lstrip("./"))
        return includeDirList

    def cmakeStringFromPathsAndIncludes(self, target,
                                        std,
                                        mainFile,
                                        fileList,
                                        testFileList,
                                        includeDirs,
                                        needTest,
                                        testonly):
        template = Template(CMAKE_TEMPLATE_STRING)
        fileListWithoutMain = [file for
                               file in fileList
                               if path.split(file)[1] != mainFile]
        return template.render(target=target,
                               std=std,
                               fileList=fileList,
                               fileListWithoutMain = fileListWithoutMain,
                               testFileList=testFileList,
                               includeDirs=includeDirs,
                               needTest=needTest,
                               testonly=testonly)


    def cmakeStringFromPath(self, target, std, mainFile, directoryPath, testonly):
        fileList = []
        testFileList = [TEST_MAIN]
        includeDirList = []
        needTest = True if self.directoryHasTestFile(glob("*.cpp")) else False
        for root, dirs, files in walk(directoryPath):
            if BUILD_DIR_STRING in root: continue
            elif TEST_DIR_STRING in root:
                #add the test files
                testFileList = self.appendCompileableFiles(testFileList,
                                                           files,
                                                           root)
            else:
                fileList = self.appendCompileableFiles(fileList,files, root)
                includeDirList = self.appendIncludeDirectory(includeDirList, files, root)
        cmakeString = self.cmakeStringFromPathsAndIncludes(target,
                                                           std,
                                                           mainFile,
                                                           fileList,
                                                           testFileList,
                                                           includeDirList,
                                                           needTest,
                                                           testonly)
        return cmakeString

    def writeCmakeFile(self, target, std, mainFile, directoryPath, testonly):
        with open("CMakeLists.txt", "w") as f:
            f.write(self.cmakeStringFromPath(target, std,  mainFile, directoryPath, testonly))

    def makeTestFile(self):
        template = Template(CATCH_TEMPLATE)
        return template.render()


def basicSetup():
    parser = argparse.ArgumentParser(description="CMakeLists.txt generator")
    parser.add_argument("-o", default="a")
    parser.add_argument("--std", default="c++11")
    parser.add_argument("--main", default="main.cpp")
    parser.add_argument("--testonly", action="store_true")
    args = parser.parse_args()
    if args.std not in CPP_STD:
        print("Bad cpp standard used, (hint use --std=c++11)")
        sys.exit(1)
    cMaker = CMakeFileMaker()
    if(args.testonly):
        if not os.path.isfile(CATCH_HPP_PATH):
            catchTestURL = urllib2.urlopen(CATCH_URL)
            with open("%s" % CATCH_HPP_PATH, "w") as f:
                f.write(catchTestURL.read())
        with open("test.cpp", "w") as f:
            f.write(cMaker.makeTestFile())
    cMaker.writeCmakeFile(args.o, args.std, args.main, ".", args.testonly)
