import numpy as np
import matplotlib.pyplot as plt

class MetropolisHastings:
    def __init__(self, target_distribution, proposal_distribution, initial_value):
        """
        Initialize the Metropolis-Hastings sampler.
        
        :param target_distribution: Function to compute the probability density of the target distribution.
        :param proposal_distribution: Function to generate a candidate from a proposal distribution.
        :param initial_value: Initial value to start the MCMC sampling.
        """
        self.target_distribution = target_distribution
        self.proposal_distribution = proposal_distribution
        self.current_value = initial_value

    def step(self):
        """
        Perform one step of the Metropolis-Hastings algorithm.
        """
        candidate = self.proposal_distribution(self.current_value)
        acceptance = min(1, self.target_distribution(candidate) / self.target_distribution(self.current_value))

        if np.random.rand() < acceptance:
            self.current_value = candidate

        return self.current_value

    def sample(self, n_samples):
        """
        Generate samples using the Metropolis-Hastings algorithm.
        
        :param n_samples: Number of samples to generate.
        :return: List of samples.
        """
        samples = [self.current_value]
        for _ in range(n_samples - 1):
            samples.append(self.step())
        return samples
    

if __name__ == "__main__":
    def normal_distribution(x):
        return np.exp(-(x-1.5)**2 / 2) / np.sqrt(2 * np.pi)

    def normal_proposal(x):
        return np.random.normal(x, 1.0)

    mh_sampler = MetropolisHastings(normal_distribution, normal_proposal, 0.0)
    samples = mh_sampler.sample(10000)

    # Plot the samples

    plt.hist(samples, bins=100, density=True)
    plt.show()
