#!/usr/bin/env python

#
# Kuroga, single python file meta-build system for ninja
# https://github.com/lighttransport/kuroga
#
# Requirements: python 2.6 or 2.7
#
# Usage: $ python kuroga.py input.py
#

import imp
import re
import textwrap
import glob
import os
import sys

# gcc preset
def add_gnu_rule(ninja):
   ninja.rule('gnucxx', description='CXX $out',
       command='$gnucxx -MMD -MF $out.d $gnudefines $gnuincludes $gnucxxflags -c $in -o $out',
       depfile='$out.d', deps='gcc')
   ninja.rule('gnucc', description='CC $out',
       command='$gnucc -MMD -MF $out.d $gnudefines $gnuincludes $gnucflags -c $in -o $out',
       depfile='$out.d', deps='gcc')
   ninja.rule('gnulink', description='LINK $out', pool='link_pool',
       command='$gnuld -o $out $in $libs $gnuldflags')
   ninja.rule('gnuar', description='AR $out', pool='link_pool',
       command='$gnuar rsc $out $in')
   ninja.rule('gnustamp', description='STAMP $out', command='touch $out')
   ninja.newline()

   ninja.variable('gnucxx', 'g++')
   ninja.variable('gnucc', 'gcc')
   ninja.variable('gnuld', '$gnucxx')
   ninja.variable('gnuar', 'ar')
   ninja.newline()

# clang preset
def add_clang_rule(ninja):
   ninja.rule('clangcxx', description='CXX $out',
       command='$clangcxx -MMD -MF $out.d $clangdefines $clangincludes $clangcxxflags -c $in -o $out',
       depfile='$out.d', deps='gcc')
   ninja.rule('clangcc', description='CC $out',
       command='$clangcc -MMD -MF $out.d $clangdefines $clangincludes $clangcflags -c $in -o $out',
       depfile='$out.d', deps='gcc')
   ninja.rule('clanglink', description='LINK $out', pool='link_pool',
       command='$clangld -o $out $in $libs $gnuldflags')
   ninja.rule('clangar', description='AR $out', pool='link_pool',
       command='$clangar rsc $out $in')
   ninja.rule('clangstamp', description='STAMP $out', command='touch $out')
   ninja.newline()

   ninja.variable('clangcxx', 'clang++')
   ninja.variable('clangcc', 'clang')
   ninja.variable('clangld', '$clangcxx')
   ninja.variable('clangar', 'ar')
   ninja.newline()

# msvc preset
def add_msvc_rule(ninja):
   ninja.rule('msvccxx', description='CXX $out',
       command='$msvccxx /TP /showIncludes $msvcdefines $msvcincludes $msvccxxflags -c $in /Fo$out',
       depfile='$out.d', deps='msvc')
   ninja.rule('msvccc', description='CC $out',
       command='$msvccc /TC /showIncludes $msvcdefines $msvcincludes $msvccflags -c $in /Fo$out',
       depfile='$out.d', deps='msvc')
   ninja.rule('msvclink', description='LINK $out', pool='link_pool',
       command='$msvcld $msvcldflags $in $libs /OUT:$out')
   ninja.rule('msvcar', description='AR $out', pool='link_pool',
       command='$msvcar $in /OUT:$out')
   #ninja.rule('msvcstamp', description='STAMP $out', command='touch $out')
   ninja.newline()

   ninja.variable('msvccxx', 'cl.exe')
   ninja.variable('msvccc', 'cl.exe')
   ninja.variable('msvcld', 'link.exe')
   ninja.variable('msvcar', 'lib.exe')
   ninja.newline()

# -- from ninja_syntax.py --
def escape_path(word):
   return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')

class Writer(object):
   def __init__(self, output, width=78):
       self.output = output
       self.width = width

   def newline(self):
       self.output.write('\n')

   def comment(self, text, has_path=False):
       for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
                                 break_on_hyphens=False):
           self.output.write('# ' + line + '\n')

   def variable(self, key, value, indent=0):
       if value is None:
           return
       if isinstance(value, list):
           value = ' '.join(filter(None, value))  # Filter out empty strings.
       self._line('%s = %s' % (key, value), indent)

   def pool(self, name, depth):
       self._line('pool %s' % name)
       self.variable('depth', depth, indent=1)

   def rule(self, name, command, description=None, depfile=None,
            generator=False, pool=None, restat=False, rspfile=None,
            rspfile_content=None, deps=None):
       self._line('rule %s' % name)
       self.variable('command', command, indent=1)
       if description:
           self.variable('description', description, indent=1)
       if depfile:
           self.variable('depfile', depfile, indent=1)
       if generator:
           self.variable('generator', '1', indent=1)
       if pool:
           self.variable('pool', pool, indent=1)
       if restat:
           self.variable('restat', '1', indent=1)
       if rspfile:
           self.variable('rspfile', rspfile, indent=1)
       if rspfile_content:
           self.variable('rspfile_content', rspfile_content, indent=1)
       if deps:
           self.variable('deps', deps, indent=1)

   def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
             variables=None):
       outputs = as_list(outputs)
       out_outputs = [escape_path(x) for x in outputs]
       all_inputs = [escape_path(x) for x in as_list(inputs)]

       if implicit:
           implicit = [escape_path(x) for x in as_list(implicit)]
           all_inputs.append('|')
           all_inputs.extend(implicit)
       if order_only:
           order_only = [escape_path(x) for x in as_list(order_only)]
           all_inputs.append('||')
           all_inputs.extend(order_only)

       self._line('build %s: %s' % (' '.join(out_outputs),
                                    ' '.join([rule] + all_inputs)))

       if variables:
           if isinstance(variables, dict):
               iterator = iter(variables.items())
           else:
               iterator = iter(variables)

           for key, val in iterator:
               self.variable(key, val, indent=1)

       return outputs

   def include(self, path):
       self._line('include %s' % path)

   def subninja(self, path):
       self._line('subninja %s' % path)

   def default(self, paths):
       self._line('default %s' % ' '.join(as_list(paths)))

   def _count_dollars_before_index(self, s, i):
       """Returns the number of '$' characters right in front of s[i]."""
       dollar_count = 0
       dollar_index = i - 1
       while dollar_index > 0 and s[dollar_index] == '$':
           dollar_count += 1
           dollar_index -= 1
       return dollar_count

   def _line(self, text, indent=0):
       """Write 'text' word-wrapped at self.width characters."""
       leading_space = '  ' * indent
       while len(leading_space) + len(text) > self.width:
           # The text is too wide; wrap if possible.

           # Find the rightmost space that would obey our width constraint and
           # that's not an escaped space.
           available_space = self.width - len(leading_space) - len(' $')
           space = available_space
           while True:
               space = text.rfind(' ', 0, space)
               if (space < 0 or
                   self._count_dollars_before_index(text, space) % 2 == 0):
                   break

           if space < 0:
               # No such space; just use the first unescaped space we can find.
               space = available_space - 1
               while True:
                   space = text.find(' ', space + 1)
                   if (space < 0 or
                       self._count_dollars_before_index(text, space) % 2 == 0):
                       break
           if space < 0:
               # Give up on breaking.
               break

           self.output.write(leading_space + text[0:space] + ' $\n')
           text = text[space+1:]

           # Subsequent lines are continuations, so indent them.
           leading_space = '  ' * (indent+2)

       self.output.write(leading_space + text + '\n')

   def close(self):
       self.output.close()


def as_list(input):
   if input is None:
       return []
   if isinstance(input, list):
       return input
   return [input]

# -- end from ninja_syntax.py --

def gen(ninja, toolchain, config):

   ninja.variable('ninja_required_version', '1.4')
   ninja.newline()

   if hasattr(config, "builddir"):
       builddir = config.builddir[toolchain]
       ninja.variable(toolchain + 'builddir', builddir)
   else:
       builddir = ''

   ninja.variable(toolchain + 'defines', config.defines[toolchain] or [])
   ninja.variable(toolchain + 'includes', config.includes[toolchain] or [])
   ninja.variable(toolchain + 'cflags', config.cflags[toolchain] or [])
   ninja.variable(toolchain + 'cxxflags', config.cxxflags[toolchain] or [])
   ninja.variable(toolchain + 'ldflags', config.ldflags[toolchain] or [])
   ninja.newline()

   if hasattr(config, "link_pool_depth"):
       ninja.pool('link_pool', depth=config.link_pool_depth)
   else:
       ninja.pool('link_pool', depth=4)
   ninja.newline()

   # Add default toolchain(gnu, clang and msvc)
   add_gnu_rule(ninja)
   add_clang_rule(ninja)
   add_msvc_rule(ninja)

   obj_files = []

   cc = toolchain + 'cc'
   cxx = toolchain + 'cxx'
   link = toolchain + 'link'
   ar = toolchain + 'ar'

   if hasattr(config, "cxx_files"):
       for src in config.cxx_files:
           srcfile = src
           obj = os.path.splitext(srcfile)[0] + '.o'
           obj = os.path.join(builddir, obj);
           obj_files.append(obj)
           ninja.build(obj, cxx, srcfile)
       ninja.newline()

   if hasattr(config, "c_files"):
       for src in config.c_files:
           srcfile = src
           obj = os.path.splitext(srcfile)[0] + '.o'
           obj = os.path.join(builddir, obj);
           obj_files.append(obj)
           ninja.build(obj, cc, srcfile)
       ninja.newline()

   targetlist = []
   if hasattr(config, "exe"):
       ninja.build(config.exe, link, obj_files)
       targetlist.append(config.exe)

   if hasattr(config, "staticlib"):
       ninja.build(config.staticlib, ar, obj_files)
       targetlist.append(config.staticlib)

   ninja.build('all', 'phony', targetlist)
   ninja.newline()

   ninja.default('all')

def main():
   if len(sys.argv) < 2:
       print("Usage: python kuroga.py config.py")
       sys.exit(1)

   config = imp.load_source("config", sys.argv[1])

   f = open('build.ninja', 'w')
   ninja = Writer(f)

   if hasattr(config, "register_toolchain"):
       config.register_toolchain(ninja)

   gen(ninja, config.toolchain, config)
   f.close()

main()