"""
NOTE: The optimal values of m and b can be calculated with less effort than doing a linear regression. 
This is just to demonstrate gradient descent.
"""
import numpy

class LinearRegression:
    """
    TODO: docstring
    """
    def __init__(self, points):
        """
        TODO: docstring
        """
        self.initial_b = 0
        self.initial_m = 0
        self.learning_rate = 0.0001
        self.num_iterations = 1000
        self.points = points

    def __call__(self):
        """
        TODO: docstring
        """
        print('Starting gradient descent at b = {0}, m = {1}, error = {2}'.format(
            self.initial_b, self.initial_m, self.compute_error(self.initial_b, self.initial_m)))
        print('Running...')
        [b, m] = self.gradient_descent()
        print('After {0} descent iterations b = {1}, m = {2}, error = {3}'.format(
            self.num_iterations, b, m, self.compute_error(b, m)))
        print('Done.')

    def compute_error(self, b, m):
        """
        Compute Error

        This function computes the error for a line using given points.

        Arguments:
            m: slope
            b: y-intercept
        """
        total_error = 0
        for i in range(len(self.points)):
            x = self.points[i, 0]
            y = self.points[i, 1]
            total_error += (y - (m * x + b)) ** 2
        return total_error / float(len(self.points))

    def gradient_descent(self):
        """
        TODO: docstring
        """
        b = self.initial_b
        m = self.initial_m
        for i in range(self.num_iterations):
            b, m = self.step_gradient(b, m, numpy.array(self.points), self.learning_rate)
        return [b, m]

    def step_gradient(self, b, m, points, learning_rate):
        """
        TODO: docstring
        """
        b_gradient, m_gradient = 0, 0
        N = float(len(points))
        for i in range(len(points)):
            x = points[i, 0]
            y = points[i, 1]
            b_gradient += -(2 / N) * 1 * (y - ((m * x) + b))
            m_gradient += -(2 / N) * x * (y - ((m * x) + b))
        b = b - (learning_rate * b_gradient)
        m = m - (learning_rate * m_gradient)
        return [b, m]

class Main:
    """
    TODO: docstring
    """
    def main():
        """
        TODO: docstring
        """
        points = numpy.genfromtxt('data\\input.csv', delimiter=',')
        linear_regression = LinearRegression(points)
        linear_regression()

if __name__ == '__main__':
    Main.main()
