"""
- Parse jit compile info
- Compute warp occupany histogram
"""
from __future__ import division, absolute_import, print_function
import math
import re

SMEM16K = 16 * 2 ** 10
SMEM48K = 48 * 2 ** 10
SMEM64K = 64 * 2 ** 10
SMEM96K = 96 * 2 ** 10
SMEM112K = 112 * 2 ** 10

#------------------------------------------------------------------------------
# autotuning


class OccupancyThreadKey(object):
    def __init__(self, item):
        self.occupancy, self.threads = item
        self.comparison = self.occupancy, 1 / self.threads

    def __lt__(self, other):
        return self.comparison < other.comparison

    def __eq__(self, other):
        return self.comparison == other.comparison

    def __ne__(self, other):
        return self.comparison != other.comparison

    def __gt__(self, other):
        return self.comparison > other.comparison

    def __le__(self, other):
        return self.comparison <= other.comparison

    def __ge__(self, other):
        return self.comparison >= other.comparison


class AutoTuner(object):
    """Autotune a kernel based upon the theoretical occupancy.
    """
    def __init__(self, cc, info, smem_config=None, dynsmem=0):
        self.cc = cc
        self.dynsmem = dynsmem
        self._table = warp_occupancy(info=info, cc=cc)
        self._by_occupancy = list(reversed(sorted(((occup, tpb)
                                                   for tpb, (occup, factor)
                                                   in self.table.items()),
                                                  key=OccupancyThreadKey)))

    @property
    def table(self):
        """A dict with thread-per-block as keys and tuple-2 of
        (occupency, limiting factor) as values.
        """
        return self._table

    @property
    def by_occupancy(self):
        """A list of tuple-2 of (occupancy, thread-per-block) sorted in
        descending.

        The first item has the highest occupancy and the lowest number of
        thread-per-block.
        """
        return self._by_occupancy

    def best(self):
        return self.max_occupancy_min_blocks()

    def max_occupancy_min_blocks(self):
        """Returns the thread-per-block that optimizes for
        maximum occupancy and minimum blocks.

        Maximum blocks allows for the best utilization of parallel execution
        because each block can be executed concurrently on different SM.
        """
        return self.by_occupancy[0][1]

    def closest(self, tpb):
        """Find the occupancy of the closest tpb
        """
        # round to the nearest multiple of warpsize
        warpsize = PHYSICAL_LIMITS[self.cc]['thread_per_warp']
        tpb = ceil(tpb, warpsize)
        # search
        return self.table.get(tpb, [0])[0]


    def best_within(self, mintpb, maxtpb):
        """Returns the best tpb in the given range inclusively.
        """
        warpsize = PHYSICAL_LIMITS[self.cc]['thread_per_warp']
        mintpb = int(ceil(mintpb, warpsize))
        maxtpb = int(floor(maxtpb, warpsize))
        return self.prefer(*range(mintpb, maxtpb + 1, warpsize))

    def prefer(self, *tpblist):
        """Prefer the thread-per-block with the highest warp occupancy
        and the lowest thread-per-block.

        May return None if all threads-per-blocks are invalid
        """
        bin = []
        for tpb in tpblist:
            occ = self.closest(tpb)
            if occ > 0:
                bin.append((occ, tpb))
        if bin:
            return sorted(bin, key=OccupancyThreadKey)[-1][1]


#------------------------------------------------------------------------------
# warp occupancy calculator

# Reference: NVIDIA CUDA Toolkit v6.5 Programming Guide, Appendix G.
# URL: http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#compute-capabilities

LIMITS_CC_20 = {
    'thread_per_warp': 32,
    'warp_per_sm': 48,
    'thread_per_sm': 1536,
    'block_per_sm': 8,
    'registers': 32768,
    'reg_alloc_unit': 64,
    'reg_alloc_gran': 'warp',
    'reg_per_thread': 63,
    'smem_per_sm': 49152,
    'smem_alloc_unit': 128,
    'warp_alloc_gran': 2,
    'max_block_size': 1024,
    'default_smem_config': SMEM16K,
}

LIMITS_CC_21 = LIMITS_CC_20

LIMITS_CC_30 = {
    'thread_per_warp': 32,
    'warp_per_sm': 64,
    'thread_per_sm': 2048,
    'block_per_sm': 16,
    'registers': 65536,
    'reg_alloc_unit': 256,
    'reg_alloc_gran': 'warp',
    'reg_per_thread': 63,
    'smem_per_sm': 49152,
    'smem_alloc_unit': 256,
    'warp_alloc_gran': 4,
    'max_block_size': 1024,
    'default_smem_config': SMEM48K,
}

LIMITS_CC_35 = LIMITS_CC_30.copy()
LIMITS_CC_35.update({
    'reg_per_thread': 255,
})

LIMITS_CC_37 = LIMITS_CC_35.copy()

LIMITS_CC_37.update({
    'registers': 131072,
    'default_smem_config': SMEM112K,
})


LIMITS_CC_50 = {
    'thread_per_warp': 32,
    'warp_per_sm': 64,
    'thread_per_sm': 2048,
    'block_per_sm': 32,
    'registers': 65536,
    'reg_alloc_unit': 256,
    'reg_alloc_gran': 'warp',
    'reg_per_thread': 255,
    'smem_per_sm': 65536,
    'smem_per_block': 49152,
    'smem_alloc_unit': 256,
    'warp_alloc_gran': 4,
    'max_block_size': 1024,
    'default_smem_config': SMEM64K,
}

LIMITS_CC_52 = LIMITS_CC_50.copy()
LIMITS_CC_52.update({
    'smem_per_sm': 98304,
    'default_smem_config': SMEM96K,
})

PHYSICAL_LIMITS = {
    (2, 0): LIMITS_CC_20,
    (2, 1): LIMITS_CC_21,
    (3, 0): LIMITS_CC_30,
    (3, 5): LIMITS_CC_35,
    (3, 7): LIMITS_CC_35,
    (5, 0): LIMITS_CC_50,
    (5, 2): LIMITS_CC_52
}


def ceil(x, s=1):
    return s * math.ceil(x / s)


def floor(x, s=1):
    return s * math.floor(x / s)


def warp_occupancy(info, cc, smem_config=None):
    """Returns a dictionary of {threadperblock: occupancy, factor}

    Only threadperblock of multiple of warpsize is used.
    Only threadperblock of non-zero occupancy is returned.
    """
    ret = {}
    try:
        limits = PHYSICAL_LIMITS[cc]
    except KeyError:
        raise ValueError("%s is not a supported compute capability"
                             % ".".join(str(c) for c in cc))
    if smem_config is None:
        smem_config = limits['default_smem_config']
    warpsize = limits['thread_per_warp']
    max_thread = info.maxthreads

    for tpb in range(warpsize, max_thread + 1, warpsize):
        result = compute_warp_occupancy(tpb=tpb,
                                        reg=info.regs,
                                        smem=info.shared,
                                        smem_config=smem_config,
                                        limits=limits)
        if result[0]:
            ret[tpb] = result
    return ret


def compute_warp_occupancy(tpb, reg, smem, smem_config, limits):
    assert limits['reg_alloc_gran'] == 'warp', \
        "assume warp register allocation granularity"
    limit_block_per_sm = limits['block_per_sm']
    limit_warp_per_sm = limits['warp_per_sm']
    limit_thread_per_warp = limits['thread_per_warp']
    limit_reg_per_thread = limits['reg_per_thread']
    limit_total_regs = limits['registers']
    limit_total_smem = min(limits['smem_per_sm'], smem_config)
    my_smem_alloc_unit = limits['smem_alloc_unit']
    reg_alloc_unit = limits['reg_alloc_unit']
    warp_alloc_gran = limits['warp_alloc_gran']

    my_warp_per_block = ceil(tpb / limit_thread_per_warp)
    my_reg_count = reg
    my_reg_per_block = my_warp_per_block
    my_smem = smem
    my_smem_per_block = ceil(my_smem, my_smem_alloc_unit)

    # allocated resource
    limit_blocks_due_to_warps = min(limit_block_per_sm,
                                    floor(
                                        limit_warp_per_sm / my_warp_per_block))

    c39 = floor(limit_total_regs / ceil(my_reg_count * limit_thread_per_warp,
                                        reg_alloc_unit),
                warp_alloc_gran)

    limit_blocks_due_to_regs = (0
                                if my_reg_count > limit_reg_per_thread
                                else (floor(c39 / my_reg_per_block)
                                      if my_reg_count > 0
                                      else limit_block_per_sm))

    limit_blocks_due_to_smem = (floor(limit_total_smem /
                                      my_smem_per_block)
                                if my_smem_per_block > 0
                                else limit_block_per_sm)

    # occupancy
    active_block_per_sm = min(limit_blocks_due_to_smem,
                              limit_blocks_due_to_warps,
                              limit_blocks_due_to_regs)

    if active_block_per_sm == limit_blocks_due_to_warps:
        factor = 'warps'
    elif active_block_per_sm == limit_blocks_due_to_regs:
        factor = 'regs'
    else:
        factor = 'smem'

    active_warps_per_sm = active_block_per_sm * my_warp_per_block
    #active_threads_per_sm = active_warps_per_sm * limit_thread_per_warp

    occupancy = active_warps_per_sm / limit_warp_per_sm
    return occupancy, factor

