// File: contracts/KofiFactory.sol



//
//  ___  __    ________  ________ ___          ________ ________  ________ _________  ________  ________      ___    ___
// |\  \|\  \ |\   __  \|\  _____\\  \        |\  _____\\   __  \|\   ____\\___   ___\\   __  \|\   __  \    |\  \  /  /|
// \ \  \/  /|\ \  \|\  \ \  \__/\ \  \       \ \  \__/\ \  \|\  \ \  \___\|___ \  \_\ \  \|\  \ \  \|\  \   \ \  \/  / /
//  \ \   ___  \ \  \\\  \ \   __\\ \  \       \ \   __\\ \   __  \ \  \       \ \  \ \ \  \\\  \ \   _  _\   \ \    / /
//   \ \  \\ \  \ \  \\\  \ \  \_| \ \  \       \ \  \_| \ \  \ \  \ \  \____   \ \  \ \ \  \\\  \ \  \\  \|   \/  /  /
//    \ \__\\ \__\ \_______\ \__\   \ \__\       \ \__\   \ \__\ \__\ \_______\  \ \__\ \ \_______\ \__\\ _\ __/  / /
//     \|__| \|__|\|_______|\|__|    \|__|        \|__|    \|__|\|__|\|_______|   \|__|  \|_______|\|__|\|__|\___/ /
//                                                                                                          \|___|/
// kofime.xyz
//
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./KofiShop.sol";

contract KofiFactory is Ownable {
	address[] private kofiShops;
	uint256 public investorFee;

	event UpdateInvestorFee(uint256 investorFee);
	event NewFactory(address owner, address kofiShop, uint256 timestamp);
	event NewFactoryFromInvestor(
		address investor,
		address kofiShop,
		uint256 amount
	);

	constructor() {
		investorFee = 10 ether;
	}

	function getKofiShops() public view returns (address[] memory) {
		return kofiShops;
	}

	function setInvestorFee(uint256 _fee) external payable onlyOwner {
		investorFee = _fee;

		emit UpdateInvestorFee(_fee);
	}

	/// @notice owner creates a kofi shop
	function create() external payable onlyOwner {
		KofiShop kofi = new KofiShop(owner(), address(0));
		kofiShops.push(address(kofi));

		emit NewFactory(owner(), address(kofi), block.timestamp);
	}

	/// @notice anyone can buy a kofi shop
	function buy() external payable {
		require(msg.value >= investorFee, "can't buy for free");

		KofiShop kofi = (new KofiShop){ value: msg.value }(owner(), msg.sender);
		kofiShops.push(address(kofi));

		emit NewFactoryFromInvestor(msg.sender, address(kofi), msg.value);
	}

	/// @notice send the entire balance stored in this contract to the owner
	function withdraw() external payable onlyOwner {
		(bool success, ) = owner().call{ value: address(this).balance }("");

		require(success, "failed to withdraw");
	}
}

// File: contracts/KofiShop.sol



//
//  ___  __    ________  ________ ___          ________  ___  ___  ________  ________
// |\  \|\  \ |\   __  \|\  _____\\  \        |\   ____\|\  \|\  \|\   __  \|\   __  \
// \ \  \/  /|\ \  \|\  \ \  \__/\ \  \       \ \  \___|\ \  \\\  \ \  \|\  \ \  \|\  \
//  \ \   ___  \ \  \\\  \ \   __\\ \  \       \ \_____  \ \   __  \ \  \\\  \ \   ____\
//   \ \  \\ \  \ \  \\\  \ \  \_| \ \  \       \|____|\  \ \  \ \  \ \  \\\  \ \  \___|
//    \ \__\\ \__\ \_______\ \__\   \ \__\        ____\_\  \ \__\ \__\ \_______\ \__\
//     \|__| \|__|\|_______|\|__|    \|__|       |\_________\|__|\|__|\|_______|\|__|
//                                               \|_________|
// kofime.xyz
//
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "./KofiShopStorage.sol";

contract KofiShop is Ownable, Pausable, KofiShopStorage {
	constructor(address _owner, address _investor) payable {
		// Store the address of the deployer as a payable address.
		// When we withdraw funds, we'll withdraw here.
		_transferOwnership(_owner);
		registrationFee = 0 ether;
		buyCoffeeFee = 0.0001 ether;
		coffeePrice = 0.02 ether;
		userCountMax = 10000;
		investor = _investor;
	}

	/// @notice fetches all stored users
	function getUsers() public view returns (User[] memory) {
		return users;
	}

	/// @notice fetches all stored memos for a user
	function getMemos(address _user) public view returns (Memo[] memory) {
		return memos[_user];
	}

	/// @notice checks if the user is blacklisted
	function isBlackListed(address _user) public view returns (uint256) {
		return blackList[_user];
	}

	/**
	 * @notice register a user
	 * @param _name name of the coffee user
	 * @param _username unique id for a user
	 */
	function register(
		string memory _name,
		string memory _username,
		string memory _bio
	) public payable whenNotPaused {
		// Must accept more than "registrationFee".
		require(msg.value >= registrationFee, "can't register for free");

		// Must have 10k users only.
		require(users.length < userCountMax, "user limit");

		// Must have unique username.
		require(
			uniqueUsernameCheck[_username] != 2,
			"username already registered"
		);

		// 1 user per address
		require(uniqueUserCheck[msg.sender] != 2, "user already registered");

		// send 30% immediately to investor.
		if (investor != address(0) && registrationFee > 0 ether) {
			(bool success, ) = investor.call{ value: (msg.value / 10) * 3 }("");

			require(success, "failed to withdraw to investor");
		}

		// Add the user to storage.
		users.push(User(msg.sender, _name, _username, _bio));

		if (users.length % 100 == 0) {
			registrationFee = registrationFee + 0.0001 ether;
		}

		uniqueUserCheck[msg.sender] = 2;
		uniqueUsernameCheck[_username] = 2;

		// Emit a NewUser event with details.
		emit NewUser(msg.sender, block.timestamp, _name, _username, _bio);
	}

	function updateUser(
		uint256 _index,
		string memory _name,
		string memory _username,
		string memory _bio
	) public whenNotPaused {
		// Must have unique username.
		require(
			uniqueUsernameCheck[_username] != 2,
			"username already registered"
		);

		// User must be registered.
		require(uniqueUserCheck[msg.sender] == 2, "not registered user");

		// Sender must be a user with _index
		require(users[_index].wallet == msg.sender, "not correct user");

		uniqueUsernameCheck[users[_index].username] = 1;
		users[_index].name = _name;
		users[_index].username = _username;
		users[_index].bio = _bio;
		uniqueUsernameCheck[_username] = 2;

		// Emit a UpdateUser event with details.
		emit UpdateUser(msg.sender, block.timestamp, _name, _username, _bio);
	}

	/**
	 * @notice buy a coffee for a user (sends eth and leaves a memo)
	 * @param _name name of the coffee purchaser
	 * @param _title title of the coffee purchaser
	 * @param _message a nice message from the purchaser
	 * @param _stars a nice rate from the purchaser
	 */
	function buyCoffee(
		address _user,
		string memory _name,
		string memory _title,
		string memory _message,
		uint256 _stars
	) public payable whenNotPaused {
		// Must accept more than "coffeePrice" for a coffee.
		require(msg.value >= coffeePrice, "can't buy coffee for free");

		// Must not be a banned user.
		require(blackList[_user] != 2, "user is banned");

		// You?
		require(msg.sender != _user, "can't buy for yourself");

		// Send coffee to user.
		(bool success, ) = _user.call{ value: msg.value - buyCoffeeFee }("");
		require(success, "failed to send ether");

		// send 50% fee immediately to investor.
		if (investor != address(0)) {
			(bool successInvestor, ) = investor.call{
				value: (buyCoffeeFee / 10) * 5
			}("");

			require(successInvestor, "failed to withdraw to investor");
		}

		// Must have less than 10 memos.
		if (memos[_user].length < 10) {
			// Add the memo to storage!
			memos[_user].push(
				Memo(msg.sender, _name, _title, _message, _stars)
			);

			// Emit a NewMemo event with details about the memo.
			emit NewMemo(
				msg.sender,
				_user,
				block.timestamp,
				_name,
				_title,
				_message,
				_stars
			);
		}

		// Emit a NewCoffee event with details.
		emit NewCoffee(msg.sender, _user, block.timestamp, msg.value);
	}

	/// Owner functions
	function pause() external payable onlyOwner {
		_pause();
	}

	function unpause() external payable onlyOwner {
		_unpause();
	}

	/**
	 * @notice removes bad memo upon request
	 * @param _index the memo id to remove
	 */
	function deleteMemo(
		uint256 _index,
		address _user
	) external payable onlyOwner {
		// Move the last memo into the place to delete
		memos[_user][_index] = memos[_user][memos[_user].length - 1];
		// Remove the last element
		memos[_user].pop();

		// Emit a DeleteMemo event
		emit DeleteMemo(_index, _user);
	}

	/**
	 * @notice sets fee of user registration
	 * @param _fee registration fee
	 */
	function setRegistrationFee(uint256 _fee) external payable onlyOwner {
		registrationFee = _fee;

		// Emit a registrationFee update event.
		emit UpdateRegistrationFee(_fee);
	}

	/**
	 * @notice sets tip for waitress
	 * @param _fee buy coffee fee
	 */
	function setBuyCoffeeFee(uint256 _fee) external payable onlyOwner {
		buyCoffeeFee = _fee;

		// Emit a buyCoffeeFee update event.
		emit UpdateBuyCoffeeFee(_fee);
	}

	/**
	 * @notice sets coffee price
	 * @param _price price of a cup of coffee
	 */
	function setCoffeePrice(uint256 _price) external payable onlyOwner {
		coffeePrice = _price;

		// Emit a coffeePrice update event.
		emit UpdateCoffeePrice(_price);
	}

	/**
	 * @notice sets max user count
	 * @param _max user count
	 */
	function setUserCountMax(uint256 _max) external payable onlyOwner {
		userCountMax = _max;

		// Emit a userCountMax update event.
		emit UpdateUserCountMax(_max);
	}

	/**
	 * @notice sets banned user
	 * @param _address address of banned user
	 * @param _ban 1 for false, 2 for true
	 */
	function setBlackList(
		address _address,
		uint256 _ban
	) external payable onlyOwner {
		blackList[_address] = _ban;

		// Emit setBlackList event.
		emit NewBannedUser(_address, _ban);
	}

	/// @notice send the entire balance stored in this contract to the owner
	function withdrawAll() external payable onlyOwner {
		(bool success, ) = owner().call{ value: address(this).balance }("");

		require(success, "failed to withdraw");
	}
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: contracts/KofiShopStorage.sol



//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract KofiShopStorage {
	struct Memo {
		address from;
		string name;
		string title;
		string message;
		uint256 stars;
	}
	struct User {
		address wallet;
		string name;
		string username;
		string bio;
	}
	User[] public users;

	// Validate user
	mapping(string => uint256) public uniqueUsernameCheck;
	mapping(address => uint256) public blackList;
	mapping(address => uint256) public uniqueUserCheck;
	mapping(address => Memo[]) public memos;

	uint256 public registrationFee;
	uint256 public buyCoffeeFee;
	uint256 public coffeePrice;
	uint256 public userCountMax;

	address public investor;

	/// @notice Event to emit when a User is created.
	event NewUser(
		address indexed user,
		uint256 timestamp,
		string name,
		string username,
		string bio
	);

	/// @notice Event to emit when a User is updated.
	event UpdateUser(
		address indexed user,
		uint256 timestamp,
		string name,
		string username,
		string bio
	);

	/// @notice Event to emit when a Memo is created.
	event NewMemo(
		address indexed from,
		address user,
		uint256 timestamp,
		string name,
		string title,
		string message,
		uint256 stars
	);

	/// @notice Event to emit when buying a cup of coffee.
	event NewCoffee(
		address indexed from,
		address user,
		uint256 timestamp,
		uint256 price
	);

	event UpdateRegistrationFee(uint256 registrationFee);
	event UpdateBuyCoffeeFee(uint256 buyFee);
	event UpdateCoffeePrice(uint256 coffeePrice);

	event UpdateUserCountMax(uint256 max);
	event NewBannedUser(address user, uint256 ban);
	event DeleteMemo(uint256 memoId, address user);
}

// File: @openzeppelin/contracts/security/Pausable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract Pausable is Context {
    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    bool private _paused;

    /**
     * @dev Initializes the contract in unpaused state.
     */
    constructor() {
        _paused = false;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    modifier whenPaused() {
        _requirePaused();
        _;
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() public view virtual returns (bool) {
        return _paused;
    }

    /**
     * @dev Throws if the contract is paused.
     */
    function _requireNotPaused() internal view virtual {
        require(!paused(), "Pausable: paused");
    }

    /**
     * @dev Throws if the contract is not paused.
     */
    function _requirePaused() internal view virtual {
        require(paused(), "Pausable: not paused");
    }

    /**
     * @dev Triggers stopped state.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}