import ctypes
import math
import os

import cpuinfo
import numpy as np
from sympy import symbols


def flatten(l):
    return [item for sublist in l for item in sublist]


def filter_ordered(elements):
    """Filter elements in a list while preserving order"""
    seen = set()
    return [e for e in elements if not (e in seen or seen.add(e))]


def convert_dtype_to_ctype(dtype):
    """Maps numpy types to C types.

    :param dtype: A Python numpy type of int32, float32, int64 or float64
    :returns: Corresponding C type
    """
    conversion_dict = {np.int32: ctypes.c_int, np.float32: ctypes.c_float,
                       np.int64: ctypes.c_int64, np.float64: ctypes.c_double}

    return conversion_dict[dtype]


def sympy_find(expr, term, repl):
    """Change all terms from function notation to array notation.

    Finds all terms of the form term(x1, x2, x3)
    and changes them to repl[x1, x2, x3]. i.e. changes from
    function notation to array notation. It also reorders the indices
    x1, x2, x3 so that the time index comes first.

    :param expr: The expression to be processed
    :param term: The pattern to be replaced
    :param repl: The replacing pattern
    :returns: The changed expression
    """

    t = symbols("t")

    if type(expr) == term:
        args_wo_t = [x for x in expr.args if x != t and t not in x.args]
        args_t = [x for x in expr.args if x == t or t in x.args]
        expr = repl[tuple(args_t + args_wo_t)]

    if hasattr(expr, "args"):
        for a in expr.args:
            expr = expr.subs(a, sympy_find(a, term, repl))

    return expr


def aligned(a, alignment=16):
    """Function to align the memmory

    :param a: The given memory
    :param alignment: Granularity of alignment, 16 bytes by default
    :returns: Reference to the start of the aligned memory
    """
    if (a.ctypes.data % alignment) == 0:
        return a

    extra = alignment / a.itemsize
    buf = np.empty(a.size + extra, dtype=a.dtype)
    ofs = (-buf.ctypes.data % alignment) / a.itemsize
    aa = buf[ofs:ofs+a.size].reshape(a.shape)
    np.copyto(aa, a)

    assert (aa.ctypes.data % alignment) == 0

    return aa


def get_optimal_block_size(shape, load_c, omp):
    """Gets optimal block size based on architecture

    :param shape: list - shape of the data buffer
    :param load_c: int - load count
    :param omp: True if OpenMP is on, else False
    :return: optimal block size

    Assuming no prefetching, square block will give the most cache reuse.
    We then take the cache/threads divide by the size of the inner most dimension in which
    we do not block. This gives us the X*Y block space, of which we take the square root
     to get the size of our blocks.

    ((C size / threads) / (4 * length inner most * kernel loads)
    """
    cache_s = int(cpuinfo.get_cpu_info()['l2_cache_size'].split(' ')[0])

    if omp:
        thread_c = os.getenv("OMP_NUM_THREADS", cpuinfo.get_cpu_info()['count'])
    else:
        thread_c = 1

    optimal_b_size = math.sqrt(
        ((1000 * cache_s) / thread_c) / (4 * shape[len(shape) - 1] * load_c))
    return int(math.ceil(optimal_b_size))

@@ -2900,16 +2900,20 @@
 ead_c = 
+int(
 os.geten
@@ -2965,16 +2965,17 @@
 count'%5D)
+)
 %0A    els
@@ -3019,16 +3019,29 @@
 b_size =
+ max(%0A       
  math.sq
@@ -3052,16 +3052,25 @@
         
+  
+  float
 ((1000 *
@@ -3129,16 +3129,29 @@
  load_c)
+%0A        ), 1
 )%0A    re

get_optimal_block_sizes minimum result must be 1