public class AppTycoonApp extends Application {

    private static AppTycoonApp sInstance;

    public static Context getContext() {
        return sInstance;
    }

    @Override
    public void onCreate() {
        sInstance = this;
        super.onCreate();
    }
} public class GameEngine {

    private static final String TAG = "GameEngine";

    private AtomicBoolean mRunning; // flag for telling if the engine has been started
    private GameState mGameState;
    private Ticker mTicker;

    private static GameEngine sInstance;

    public static synchronized GameEngine getInstance() {
        if (sInstance == null) {
            sInstance = new GameEngine();
        }
        return sInstance;
    }


    private GameEngine() {
        mRunning = new AtomicBoolean(false);
    }


    /**
     * Loads a set of game data used for testing and development.
     */
    public void loadTestData() {
        mGameState = new GameState();
        Company company = new Company("Company Name Inc.", 10, 25000000, 90000000000L);

        // Add project slots
        for (int i = 0; i < 3; i++) {
            company.addProjectSlot();
        }

        // Add premises asset
        company.addAsset(PremisesAsset.getPremisesAssetById(PremisesAsset.SMALL_OFFICE_SPACE));

        // Add employees
        company.addEmployee(EmployeeType.TYPE_DEVELOPER, 3);
        //company.addEmployee(EmployeeType.TYPE_SENIOR_DEVELOPER, 2);
        //company.addEmployee(EmployeeType.TYPE_TEST_ENGINEER, 0);

        // Add products
        Product mobileApp = new Product("NameOfTheApp", ProductType.getProductType(ProductType.PRODUCT_TYPE_MOBILE_APP));
        mobileApp.rebuildNewProductDevelopmentProject();
        company.addProduct(mobileApp);

        mGameState.setCompany(company);
    }


    /**
     * Starts the game engine and begins simulating the game with loaded GameState.
     */
    public void startEngine() {
        if (mRunning.getAndSet(true)) {
            Log.e(TAG, "startEngine() - Engine is already started.");
            return;
        }

        // Start the ticker
        mTicker = new Ticker();
        new Thread(mTicker).start();
    }


    /**
     * First gracefully prepares the current GameState for an exit. Then stops the game engine.
     */
    public void stopEngine() {
        if (!mRunning.getAndSet(false)) {
            Log.e(TAG, "stopEngine() - Engine is already stopped.");
            return;
        }

        // Stop the ticker
        mTicker.stop();
    }

    public GameState getGameState() {
        return mGameState;
    }

    /**
     * Use this only when setting current GameState to one that is loaded
     * from storage of cloud.
     *
     * @param gameState GameState that is to be loaded.
     */
    public void setGameState(GameState gameState) {
        mGameState = gameState;
    }
} public class Product {

    private static final String TAG = "Product";

    private String mName;

    /** Number of releases of this product */
    private int mReleaseCount;

    /** The main type */
    private ProductType mType;

    /** Features and their current levels */
    private List<Pair<ProductFeature, Integer>> mFeatures;

    /** Sub-products */
    private List<Product> mSubProducts;

    /** The version of this product currently in distribution */
    private Product mReleasedVersion;

    /** Calculated complexity of the product */
    private long mComplexity;

    /** Current quality of the product */
    private long mQuality;

    /** Number of active users the product has */
    private long mUsers;

    /** Number of all-time downloads (or purchases for a paid product) */
    private long mDownloads;

    /** Money made from one sold unit */
    private int mUnitPrice;

    /** Amount of money the product has earned all-time */
    private long mTotalEarnings;

    /**
     * Array of all the features that directly generate income.
     * Key of the array is the ID of the feature.
     *
     * All income values are money per second per active user.
     */
    private SparseArray<Double> mFeatureIncomes;

    /** The development project for the next release. This will be null until the next version
     * is defined. Be sure to nullify this after the project is done!
     */
    private ProductDevelopmentProject mDevProject;

    public Product(String name, ProductType type) {
        mName = name;
        mType = type;
        mReleaseCount = 0;
        mComplexity = 0;
        mQuality = 0;

        mUnitPrice = 0;
        mDownloads = 0;
        mTotalEarnings = 0;
        mUsers = 1288; // TODO: Set to something for dev purposes

        mFeatures = new ArrayList<>();
        mFeatureIncomes = new SparseArray<>();
        mSubProducts = new ArrayList<>();

        // Every product has the "Core functionality" feature. So, we add it here.
        ProductFeature coreFeatures = ProductFeature.getFeature(ProductFeature.PRODUCT_FEATURE_CORE_FEATURES);
        mFeatures.add(new Pair<>(coreFeatures, 1));

        calculateComplexity();
    }

    /**
     * Calculates the complexity of the product and stores it into {@link #mComplexity}.
     * This should be called always after the product is changed so that the complexity potentially
     * changes as a result.
     * @return The calculated complexity
     */
    private long calculateComplexity() {
        mComplexity = mType.getBaseComplexity();

        // Re-calculate complexity also for subproducts.
        for (Product subProduct : mSubProducts) {
            mComplexity += subProduct.calculateComplexity();
        }

        // Add complexity effect of features.
        for (Pair<ProductFeature, Integer> featureAndLevel : mFeatures) {
            mComplexity += featureAndLevel.first.getComplexityForLevel(featureAndLevel.second);
        }

        return mComplexity;
    }

    /**
     * Takes care of all the housekeeping needed for releasing the next version.
     */
    public void releaseNextVersion() {
        Log.d(TAG, "releaseNextVersion()");
        mReleaseCount++;
        mDevProject = null;
        mReleasedVersion = cloneForStorage();
    }

    /**
     * Returns the version that's been released and currently in distribution.
     *
     * @return Product object representing the version that's released
     */
    public Product getReleasedVersion() {
        Log.d(TAG, "getReleasedVersion() - null? " + (mReleasedVersion == null));
        return mReleasedVersion;
    }

    public String getName() {
        return mName;
    }

    public void setName(String name) {
        mName = name;
    }

    public long getQuality() {
        return mQuality;
    }

    public int getReleaseCount() {
        return mReleaseCount;
    }

    public ProductDevelopmentProject getDevelopmentProject() {
        return mDevProject;
    }

    public void setDevelopmentProject(ProductDevelopmentProject project) {
        mDevProject = project;
    }

    public List<Pair<ProductFeature, Integer>> getFeatures() {
        return mFeatures;
    }

    /**
     * Searches the feature from List<Pair<ProductFeature, Integer>> and
     * gets its level.
     *
     * TODO: Remove this method when ProductFeature is refactored.
     *
     * @param feature Feature to look the level for.
     * @return The level of the feature. -1 in case the feature was not found.
     */
    public int getLevelOfAFeature(ProductFeature feature) {
        for (Pair<ProductFeature, Integer> pair : mFeatures) {
            if (pair.first.equals(feature)) {
                return pair.second;
            }
        }

        return -1;
    }

    /**
     * Builds a single list of all the features.
     *
     * TODO: Remove this method when ProductFeature is refactored.
     * @return The list with all the features.
     */
    public List<ProductFeature> getFeaturesAsAList() {
        ArrayList<ProductFeature> features = new ArrayList<>();

        for (Pair<ProductFeature, Integer> pair : mFeatures) {
            features.add(pair.first);
        }

        return features;
    }

    public ProductType getType() {
        return mType;
    }

    public long getComplexity() {
        return mComplexity;
    }

    /**
     * Add or upgrade a feature for a product. If the feature already exists for the product, it
     * will be replaced with the new one with the given level.
     *
     * @param feature Feature to add or replace
     * @param level Level of the feature
     */
    public void addFeature(ProductFeature feature, int level) {
        Log.d(TAG, "addFeature() - adding '" + feature.getName() + "', level " + level);

        // Use iterator instead of a foreach loop in order to allow removing items from the list
        // while iterating through it.
        Iterator<Pair<ProductFeature, Integer>> iterator = mFeatures.iterator();
        while (iterator.hasNext()) {
            Pair<ProductFeature, Integer> pair = iterator.next();
            ProductFeature existingFeature = pair.first;
            if (feature.equals(existingFeature)) {
                Log.d(TAG, "addFeature() - removed existing '" + existingFeature + "'");
                iterator.remove();
            }
        }
        mFeatures.add(new Pair<>(feature, level));

        // In case of a feature that gives income (ads, in-app purchases, subscriptions, ...),
        // handle the change in income for this product.
        if (feature.isIncomeFeature()) {
            handleIncomeFeature(feature);
        }

        // Recalculate the complexity of this product.
        calculateComplexity();
    }

    /**
     * Remove a feature from the product.
     *
     * @param feature Feature to remove.
     */
    public void removeFeature(ProductFeature feature) {

        Pair<ProductFeature, Integer> pairToRemove = null;

        for (Pair<ProductFeature, Integer> pair : mFeatures) {
            if (pair.first.equals(feature)) {
                pairToRemove = pair;
                break;
            }
        }

        if (pairToRemove != null) {
            mFeatures.remove(pairToRemove);
            Log.d(TAG, "removeFeature() - removed feature '" + pairToRemove.first.getName() + "'");
        } else {
            Log.e(TAG, "removeFeature() - tried to remove a non-existing feature");
        }

        calculateComplexity();

    }

    /**
     * Handles the change in income by incrementing the correct income value for a given income
     * feature.
     *
     * Call this method with the feature giving income whenever it is added.
     *
     * @param feature A feature that causes the change in income. This should be an income feature.
     */
    private void handleIncomeFeature(ProductFeature feature) {
        if (!feature.isIncomeFeature()) {
            Log.d(TAG, "handleIncomeFeature() - feature id " + feature.getFeatureId() + " is not" +
                    "an income feature -> did nothing");
            return;
        }

        // TODO: Get the correct income for the feature per second per user. Now it's just 0.001 * [level of the feature].
        mFeatureIncomes.append(feature.getFeatureId(), (double) getLevelOfAFeature(feature) * 0.001d);
    }

    /**
     * Returns the total income this product currently generates per second.
     *
     * @return Income per second
     */
    public double getIncomePerSecond() {
        double sum = 0.0f;

        //
        // Add income from features that generate income.

        // Check for all the income features if this product has them.
        for (int i : ProductFeature.getIncomeFeatureIds()) {
            Double income = mFeatureIncomes.get(i);

            // Variable 'income' is now null if this product did not have the feature.
            sum += (income == null ? 0.0f : income) * (double) mUsers;
        }

        return sum;
    }

    /**
     * Rebuilds the development project for a new product based on the type and
     * features of this product.
     *
     * Call this when the product or its features change during configuration of
     * a new product.
     *
     * If the project is not already created, this creates it.
     */
    public void rebuildNewProductDevelopmentProject() {
        if (mDevProject == null) {
            Log.d(TAG, "rebuildNewProductDevelopmentProject() - new project created");
            mDevProject = new ProductDevelopmentProject("no description", this);
        }
        Log.d(TAG, "rebuildNewProductDevelopmentProject() - start");

        // Set the project description to include the product name.
        mDevProject.setDescription(getName());

        mDevProject.removeAllTasks();

        // Add the task for the initial product development.
        mDevProject.addTask(new NewProductTask(this));

        // Add tasks for features.
        for (Pair<ProductFeature, Integer> pair : mFeatures) {
            ProductFeature feature = pair.first;
            int level = pair.second;

            FeatureTask featureTask = new FeatureTask(feature, 0, level);
            mDevProject.addTask(featureTask);
        }

        // Recalculate the work amount for the project.
        mDevProject.updateWorkAmount();

        Log.d(TAG, "rebuildNewProductDevelopmentProject() - work amount of the project is " +
                mDevProject.getWorkAmount());
    }

    /** Rebuilds the development project for the next version of a product based on difference of
     * the current product configuration to that of the previously released.
     *
     * Call this when the product or its features change during configuration of the next release.
     *
     * If the project is not already created, this will create it.
     */
    public void rebuildNewVersionDevelopmentProject() {
        if (mDevProject == null) {
            Log.d(TAG, "rebuildNewVersionDevelopmentProject() - new project created");
            mDevProject = new ProductDevelopmentProject(getName() + " v"
                    + (getReleaseCount() + 1), this);
        }

        Log.d(TAG, "rebuildNewVersionDevelopmentProject() - start");

        // First clear all the tasks from the project.
        mDevProject.removeAllTasks();

        // Then add feature tasks based on the difference of this product to the previous version.
        // Iterate through each feature of this product and see if it exists on the previous
        // and by which level.
        for (Pair<ProductFeature, Integer> pair : mFeatures) {
            ProductFeature feature = pair.first;
            int level = pair.second;
            boolean newFeature = true;
            FeatureTask task = null;

            for (Pair<ProductFeature, Integer> prevPair : mReleasedVersion.getFeatures()) {
                ProductFeature prevFeature = prevPair.first;
                int prevLevel = prevPair.second;

                // If the feature exists also on the previous version, and it's leveled up,
                // create a task for leveling the feature for the level difference.
                if (feature.equals(prevFeature)) {
                    if (level > prevLevel) {
                        Log.d(TAG, "rebuildNewProductDevelopmentProject() - level up '" +
                                    feature.getName() + "' from " + prevLevel + " to " + level);
                        task = new FeatureTask(feature, prevLevel, level);
                    }
                    newFeature = false;
                    break;
                }
            }

            // If the feature was not found on the previous version of the product, then the
            // newFeature flag will be up at this point. Create a task for a new feature in this case.
            if (newFeature) {
                Log.d(TAG, "rebuildNewProductDevelopmentProject() - new feature '" +
                            feature.getName() + "'");
                task = new FeatureTask(feature, 0, level);
            }

            // If a task is defined at this point, it means that either a new feature was added, or
            // an existing feature was leveled up. Add the task to the project, if it was defined.
            if (task != null) {
                Log.d(TAG, "rebuildNewProductDevelopmentProject() - adding task '" +
                            task.getTaskTitle() + "'");
                mDevProject.addTask(task);
            }
        }

        // TODO: Add a re-branding task in case the product name was changed.

        // TODO: Add a bug fixing phase in case bugs are marked to be fixed.

        mDevProject.updateWorkAmount();
        Log.d(TAG, "rebuildNewProductDevelopmentProject() - done. work amount of the project is " +
                mDevProject.getWorkAmount());
    }

    /**
     * Gets a specific product feature of the product together with its level.
     *
     * @param feature ID of the feature to get
     * @return A Pair with the feature and its level, null if the product does not have the searched feature.
     */
    public Pair<ProductFeature, Integer> getFeature(int feature) {
        for (Pair<ProductFeature, Integer> pair : getFeatures()) {
            if (pair.first.getFeatureId() == feature) {
                return pair;
            }
        }
        return null;
    }

    /**
     * Clones the current Product object and stores all the information, that is necessary to
     * be stored (for keeping track of different release versions of the same product), into a
     * new object.
     *
     * @return A new object with all the necessary fields copied.
     */
    private Product cloneForStorage() {
        Log.d(TAG, "cloneForStorage()");
        Product ret = new Product(mName, mType);

        ret.mReleaseCount = mReleaseCount;
        ret.mComplexity = mComplexity;
        ret.mQuality = mQuality;
        ret.mUnitPrice = mUnitPrice;
        ret.mFeatures = new ArrayList<>(mFeatures);
        ret.mFeatureIncomes = mFeatureIncomes.clone();
        ret.mSubProducts = new ArrayList<>(mSubProducts);

        return ret;
    }

    //
    //
    // Static utility methods
    //
    //

    /**
     * Generates a random product.
     *
     * @return Generated product
     *
     */
    public static Product getRandomProduct() {
        // Get a random type that is suitable to be a parent.
        ProductType type = ProductType.getRandomProductType();
        while (type.mustHaveParent()) {
            type = ProductType.getRandomProductType();
        }
        Product product = new Product("noname", type);

        Random r = new Random();

        // Set a random set of features for the product.
        List<ProductFeature> features = type.getPossibleFeatures();
        int numOfFeatures = r.nextInt(features.size() + 1);

        Log.d(TAG, "getRandomProduct() - type has " + features.size() + " features.");
        Log.d(TAG, "getRandomProduct() - add " + numOfFeatures + " features.");

        for (int i = 0; i < numOfFeatures; i++) {
            // Feature level is between 1 and 5.
            int featureLevel = r.nextInt(4) + 1;
            ProductFeature feature = features.get(r.nextInt(features.size()));
            product.addFeature(feature, featureLevel);
        }

        product.calculateComplexity();

        return product;
    }

} public abstract class ProductFeature {

    private static final String TAG = "ProductFeature";

    /** ID of the feature */
    private int mFeatureId;

    /** Human readable name of the feature */
    private String mName;

    /** Flag for telling if this feature can be leveled up. */
    private boolean mCanBeLeveledUp;

    /**
     * This is the "default" constructor. This constructs a ProductFeature that can be leveled
     * up.
     *
     * @param featureId Numeric ID of the feature.
     * @param name Human-readable name of the feature.
     */
    private ProductFeature(int featureId, String name) {
        mFeatureId = featureId;
        mName = name;
        mCanBeLeveledUp = true;
    }

    /**
     * Constructor with the boolean param for toggling is the feature can be leveled up or not.
     *
     * @param featureId Numeric ID of the feature.
     * @param name Human-readable name of the feature.
     * @param canBeLeveledUp True when the feature can be leveled up. False when not.
     */
    private ProductFeature(int featureId, String name, boolean canBeLeveledUp) {
        this(featureId, name);
        mCanBeLeveledUp = canBeLeveledUp;
    }

    /**
     * Return the amount of complexity this feature adds to the product at a specific feature
     * level.
     * @param level Level for which to get the complexity gain.
     * @return Amount of complexity this feature adds to the product.
     */
    public abstract int getComplexityForLevel(int level);

    /**
     * Return the amount of work (or complexity) needed to upgrade this feature from one level
     * to another. By default this is just sequentially upgrading one level at a time. But
     * this can be overridden to provide different kinds of formulas for calculating this.
     * @param fromLevel From which level of the feature to upgrade.
     * @param toLevel To which level to upgrade.
     * @return The amount of complexity that the upgrade costs.
     */
    public int getComplexityForUpgrade(int fromLevel, int toLevel) {
        int ret = 0;
        for (int i = fromLevel + 1; i <= toLevel; i++) {
            ret += getComplexityForLevel(i);
        }
        return ret;
    }

    public int getFeatureId() {
        return mFeatureId;
    }

    public String getName() {
        return mName;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ProductFeature)) {
            return false;
        }
        int objId = ((ProductFeature) obj).mFeatureId;
        return mFeatureId == objId;
    }

    @Override
    public String toString() {
        return mName;
    }

    /**
     * Checks if this feature is one that directly gives income.
     *
     * @return True when this is a income feature, false otherwise
     */
    public boolean isIncomeFeature() {
        initStaticContent();
        return sIncomeFeatures.contains(mFeatureId);
    }

    /**
     * @return List of feature IDs of features that directly generate income.
     */
    public static List<Integer> getIncomeFeatureIds() {
        initStaticContent();
        return sIncomeFeatures;
    }

    //
    //
    // Static content
    public static final int PRODUCT_FEATURE_CORE_FEATURES = 1;
    public static final int PRODUCT_FEATURE_UI_ANIMATIONS = 10;
    public static final int PRODUCT_FEATURE_RESPONSIVE_DESIGN = 20;
    public static final int PRODUCT_FEATURE_SOCIAL_MEDIA_INTEGRATION = 30;

    public static final int PRODUCT_FEATURE_BANNER_ADS = 100;
    public static final int PRODUCT_FEATURE_VIDEO_ADS = 110;
    public static final int PRODUCT_FEATURE_IN_APP_PURCHASES = 120;
    public static final int PRODUCT_FEATURE_PAID_SUBSCRIPTIONS = 130;

    private static SparseArray<ProductFeature> sProductFeatures;
    private static List<Integer> sIncomeFeatures; // Features that directly give income.

    public static ProductFeature getFeature(int feature) {
        initStaticContent();
        return sProductFeatures.get(feature);
    }

    public static void initStaticContent() {
        if (sProductFeatures != null && sIncomeFeatures != null) {
            // Init needed only once
            return;
        }

        sProductFeatures = new SparseArray<>();
        sIncomeFeatures = new ArrayList<>();

        // Add income feature IDs to the list.
        sIncomeFeatures.add(PRODUCT_FEATURE_BANNER_ADS);
        sIncomeFeatures.add(PRODUCT_FEATURE_VIDEO_ADS);
        sIncomeFeatures.add(PRODUCT_FEATURE_IN_APP_PURCHASES);
        sIncomeFeatures.add(PRODUCT_FEATURE_PAID_SUBSCRIPTIONS);


        sProductFeatures.append(PRODUCT_FEATURE_CORE_FEATURES, new ProductFeature(PRODUCT_FEATURE_CORE_FEATURES, "Core Functionality") {
            @Override
            public int getComplexityForLevel(int level) {
                return level * (1000 + level);
            }
        });

        sProductFeatures.append(PRODUCT_FEATURE_UI_ANIMATIONS, new ProductFeature(PRODUCT_FEATURE_UI_ANIMATIONS, "UI Animations") {
            @Override
            public int getComplexityForLevel(int level) {
                return level * (1000 + level);
            }

        });

        sProductFeatures.append(PRODUCT_FEATURE_RESPONSIVE_DESIGN, new ProductFeature(PRODUCT_FEATURE_RESPONSIVE_DESIGN, "Responsive Design") {
            @Override
            public int getComplexityForLevel(int level) {
                return level * (800 + level);
            }
        });

        sProductFeatures.append(PRODUCT_FEATURE_SOCIAL_MEDIA_INTEGRATION, new ProductFeature(PRODUCT_FEATURE_SOCIAL_MEDIA_INTEGRATION, "Social Media Integration") {
            @Override
            public int getComplexityForLevel(int level) {
                return level * (2000 + level * 2);
            }
        });

        sProductFeatures.append(PRODUCT_FEATURE_BANNER_ADS, new ProductFeature(PRODUCT_FEATURE_BANNER_ADS, "Banner Ads", false) {
            @Override
            public int getComplexityForLevel(int level) {
                return 5000;
            }
        });

        sProductFeatures.append(PRODUCT_FEATURE_VIDEO_ADS, new ProductFeature(PRODUCT_FEATURE_VIDEO_ADS, "Video Ads", false) {
            @Override
            public int getComplexityForLevel(int level) {
                return 9000;
            }
        });

        sProductFeatures.append(PRODUCT_FEATURE_IN_APP_PURCHASES, new ProductFeature(PRODUCT_FEATURE_IN_APP_PURCHASES, "In-app Purchases", false) {
            @Override
            public int getComplexityForLevel(int level) {
                return 8000;
            }
        });

        sProductFeatures.append(PRODUCT_FEATURE_PAID_SUBSCRIPTIONS, new ProductFeature(PRODUCT_FEATURE_PAID_SUBSCRIPTIONS, "Paid Subcriptions", false) {
            @Override
            public int getComplexityForLevel(int level) {
                return 85000;
            }
        });


    }

} public class ProductType {

    /** Type ID of the type */
    private int mType;

    /** Human readable name of the product type */
    private String mName;

    /** Flag for telling if this type can only exist as a child. */
    private boolean mMustHaveParent;

    /** Base complexity of the type */
    private long mBaseComplexity;

    /** Possible subtypes for this type. */
    private List<ProductType> mPossibleSubTypes;

    /** If this list has entries, it means that a product of this type cannot
     * be a product without sub-products of at least these sub-types. */
    private List<ProductType> mRequiredSubTypes;

    /** List of features a product of this type can have. */
    private List<ProductFeature> mPossibleFeatures;

    /** Constructor is private as instances are only built once by
     * the {@link #initStaticContent()} method.
     * @param type Type ID of the type
     * @param name Human readable name of the type
     */
    private ProductType(int type, String name, long baseComplexity) {
        mName = name;
        mPossibleSubTypes = new ArrayList<>();
        mRequiredSubTypes = new ArrayList<>();
        mPossibleFeatures = new ArrayList<>();
        mMustHaveParent = false;
        mBaseComplexity = baseComplexity;
        mType = type;

        addTypeToArrays(type, this);
    }

    /**
     * This is the constructor called for subtypes as this includes
     * the mustHaveParent parameter.
     *
     * @param type Type ID of the type
     * @param name Human readable name of the type
     * @param mustHaveParent True when this type cannot be the parent type on its own
     * @param baseComplexity Minimum amount of complexity a product of this type has
     */
    private ProductType(int type, String name, boolean mustHaveParent, long baseComplexity) {
        this(type, name, baseComplexity);
        mMustHaveParent = mustHaveParent;
    }

    public boolean mustHaveParent() {
        return mMustHaveParent;
    }

    public String getName() {
        return mName;
    }

    private ProductType addPossibleSubType(ProductType subType) {
        mPossibleSubTypes.add(subType);
        return this;
    }

    private ProductType addRequiredSubType(ProductType subType) {
        mRequiredSubTypes.add(subType);
        return this;
    }

    private ProductType addPossibleFeature(int featureId) {
        ProductFeature feature = ProductFeature.getFeature(featureId);
        mPossibleFeatures.add(feature);
        return this;
    }

    public int getType() {
        return mType;
    }

    @Override
    public String toString() {
        return mName;
    }

    public long getBaseComplexity() {
        return mBaseComplexity;
    }

    public List<ProductFeature> getPossibleFeatures() {
        return mPossibleFeatures;
    }

    //
    //
    // Static content.
    public static final int PRODUCT_TYPE_WEB_APP = 10;
    public static final int PRODUCT_TYPE_MOBILE_APP = 20;
    public static final int PRODUCT_TYPE_MOBILE_GAME = 25;
    public static final int PRODUCT_TYPE_DESKTOP_APP = 30;
    public static final int PRODUCT_TYPE_SERVER_BACKEND = 40;

    private static SparseArray<ProductType> sProductTypes;
    private static ArrayList<Integer> sProductTypeIds;

    public static ProductType getProductType(int type) {
        initStaticContent();
        return sProductTypes.get(type);
    }

    private static void initStaticContent() {
        if (sProductTypes != null) {
            // Don't init statics more than once.
            return;
        }

        sProductTypes = new SparseArray<>();
        sProductTypeIds = new ArrayList<>();

        ProductType webApp = new ProductType(PRODUCT_TYPE_WEB_APP, "Web App", 3000);
        ProductType mobileApp = new ProductType(PRODUCT_TYPE_MOBILE_APP, "Mobile App", 4000);
        ProductType mobileGame = new ProductType(PRODUCT_TYPE_MOBILE_GAME, "Mobile Game", 4500);
        ProductType desktopApp = new ProductType(PRODUCT_TYPE_DESKTOP_APP, "Desktop App", 5000);
        ProductType serverBackend = new ProductType(PRODUCT_TYPE_SERVER_BACKEND, "Server Backend", true, 2000);

        webApp.addPossibleSubType(serverBackend)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_RESPONSIVE_DESIGN)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_SOCIAL_MEDIA_INTEGRATION)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_UI_ANIMATIONS);

        mobileApp.addPossibleSubType(serverBackend)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_RESPONSIVE_DESIGN)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_SOCIAL_MEDIA_INTEGRATION)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_UI_ANIMATIONS)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_PAID_SUBSCRIPTIONS);

        desktopApp.addPossibleSubType(serverBackend)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_RESPONSIVE_DESIGN)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_SOCIAL_MEDIA_INTEGRATION)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_UI_ANIMATIONS);

        mobileGame.addPossibleSubType(serverBackend)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_BANNER_ADS)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_VIDEO_ADS)
            .addPossibleFeature(ProductFeature.PRODUCT_FEATURE_IN_APP_PURCHASES);

        /*
        addTypeToArrays(PRODUCT_TYPE_WEB_APP, webApp);
        addTypeToArrays(PRODUCT_TYPE_MOBILE_APP, mobileApp);
        addTypeToArrays(PRODUCT_TYPE_MOBILE_GAME, mobileGame);
        addTypeToArrays(PRODUCT_TYPE_DESKTOP_APP, desktopApp);
        addTypeToArrays(PRODUCT_TYPE_SERVER_BACKEND, serverBackend);
        */

    }

    /**
     * Adds product type ID and product type into respective lists. This is used for initializing
     * static content.
     * @param typeId ID constant of the type
     * @param type The ProductType
     */
    private static void addTypeToArrays(int typeId, ProductType type) {
        sProductTypes.append(typeId, type);
        sProductTypeIds.add(typeId);
    }

    /**
     * Gets a random ProductType from all the available ones.
     * @return A random ProductType
     */
    public static ProductType getRandomProductType() {
        initStaticContent();
        int typeId = (Integer) Utils.getRandomFromList(sProductTypeIds);
        return sProductTypes.get(typeId);
    }

    /**
     * Builds a list of all the product types.
     * @return The list with all the product types.
     */
    public static List<ProductType> getAllTypes() {
        initStaticContent();
        ArrayList<ProductType> types = new ArrayList<>();

        for (int typeId : sProductTypeIds) {
            types.add(sProductTypes.get(typeId));
        }

        return types;
    }


} public class ProductDevelopmentProject extends Project {

    private Product mProduct;
    private double mFractionalQuality;
    private long mQualityGain;

    public ProductDevelopmentProject(String description, Product product) {
        // Name of the new product dev. project is "New [product type]"
        super("R&D - " + product.getType().getName(), description);
        mProduct = product;
    }

    @Override
    protected long calculateWorkAmount() {

        long workAmount = 0;

        // Work amount is the sum of work amounts of all the tasks.
        for (ProjectTask task : mTasks) {
            workAmount += task.getWorkAmount();
        }

        return workAmount;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    /** Delivering the project releases the new version of the product. */
    @Override
    public void deliver() {
        mProduct.releaseNextVersion();
        GameEngine.getInstance().getGameState().getCompany().onIncomeChanged();

        Toast.makeText(AppTycoonApp.getContext(),
                mProduct.getName() + " released",
                Toast.LENGTH_LONG).show();
    }

    public void removeAllTasks() {
        mTasks = new ArrayList<>();
    }

    @Override
    public void progress(double workAmount, double qualityAmount, long time) {
        super.progress(workAmount, qualityAmount, time);

        // Add quality gain.
        mFractionalQuality += qualityAmount;
        long qualityIntPart = (long) mFractionalQuality;
        mFractionalQuality -= qualityIntPart;
        mQualityGain += qualityIntPart;

    }

    public long getQualityGain() {
        return mQualityGain;
    }
} public class MainActivity extends FragmentActivity implements UiUpdater {

    private static final String TAG = "MainActivity";

    public static final int FRAGMENT_PROJECTS = 0;
    public static final int FRAGMENT_NEW_PROJECT = 1;

    public static final int FRAGMENT_HUMAN_RESOURCES = 10;

    public static final int FRAGMENT_PRODUCTS = 20;
    public static final int FRAGMENT_NEW_PRODUCT = 21;
    public static final int FRAGMENT_PRODUCT_STATS = 22;
    public static final int FRAGMENT_PRODUCT_NEW_RELEASE = 23;

    public static final int FRAGMENT_ASSETS = 30;
    public static final int FRAGMENT_PREMISES = 31;
    public static final int FRAGMENT_IT = 32;

    /** Currently visible fragment. */
    private int mCurrentFragment = Integer.MIN_VALUE;

    /** IDs of the fragments that the user navigated to.
     * Used for providing "back" action.
     */
    private ArrayList<Integer> mNavigationStack = new ArrayList<>();

    /** A list containing all the navigation drawer TextViews.
     * This is used to control their appearance based on which fragment
     * is visible. */
    private ArrayList<TextView> mNavigationTextViews = new ArrayList<>();

    private DrawerLayout mDrawerLayout;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //
        // Start the game engine
        final GameEngine engine = GameEngine.getInstance();
        engine.loadTestData();
        engine.startEngine();

        setContentView(R.layout.activity_main);

        RelativeLayout topBar = (RelativeLayout) findViewById(R.id.layoutTopBar);
        topBar.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDrawerLayout.openDrawer(GravityCompat.START);
            }
        });

        //
        // Navigation drawer item handling
        mDrawerLayout = (DrawerLayout) findViewById(R.id.layoutDrawer);
        setNavItemListeners((TextView) findViewById(R.id.textViewNavProjects), FRAGMENT_PROJECTS);
        setNavItemListeners((TextView) findViewById(R.id.textViewNavProducts), FRAGMENT_PRODUCTS);
        setNavItemListeners((TextView) findViewById(R.id.textViewNavEmployees), FRAGMENT_HUMAN_RESOURCES);
        setNavItemListeners((TextView) findViewById(R.id.textViewNavAssets), FRAGMENT_ASSETS);

        // Go to the Projects fragment on startup
        findViewById(R.id.textViewNavProjects).performClick();

        updateUi(Integer.MIN_VALUE, null);

    }

    @Override
    protected void onResume() {
        Log.d(TAG, "onResume()");
        super.onResume();
        UiUpdateHandler uiUpdateHandler = UiUpdateHandler.getInstance();
        uiUpdateHandler.registerUpdater(this, UiUpdateHandler.ACTION_REPLACE_FRAGMENT);
        uiUpdateHandler.registerUpdater(this, UiUpdateHandler.ACTION_GO_BACK);
        uiUpdateHandler.registerUpdater(this, UiUpdateHandler.ACTION_REFRESH_UI);
    }

    @Override
    protected void onPause() {
        Log.d(TAG, "onPause()");
        super.onPause();
        UiUpdateHandler uiUpdateHandler = UiUpdateHandler.getInstance();
        uiUpdateHandler.unRegisterUpdater(this);
    }

    /**
     * Switches the content fragment to another one. Also passes args to the fragment if the
     * fragment needs them.
     * @param fragment Const of the fragment to switch to.
     */
    private void switchFragment(int fragment, Bundle args) {
        Log.d(TAG, "switchFragment() - fragment = " + fragment +
            ", has args = " + (args != null));

        // If the target fragment is the same as current one, do nothing.
        if (fragment == mCurrentFragment) {
            Log.d(TAG, "switchFragment() - already at target fragment!");
            return;
        }

        Fragment newFragment = null;

        switch(fragment) {
            case FRAGMENT_PROJECTS:
                newFragment = new ProjectsFragment();
                break;
            case FRAGMENT_HUMAN_RESOURCES:
                newFragment = new EmployeesFragment();
                break;
            case FRAGMENT_NEW_PROJECT:
                newFragment = new NewProjectFragment();
                break;
            case FRAGMENT_PRODUCTS:
                newFragment = new ProductsFragment();
                break;
            case FRAGMENT_NEW_PRODUCT:
                newFragment = new NewProductFragment();
                break;
            case FRAGMENT_ASSETS:
                newFragment = new AssetsFragment();
                break;
            case FRAGMENT_PREMISES:
                newFragment = new PremisesFragment();
                break;
            case FRAGMENT_IT:
                newFragment = new ItFragment();
                break;
            case FRAGMENT_PRODUCT_STATS:
                newFragment = new ProductStatsFragment();
                break;
            case FRAGMENT_PRODUCT_NEW_RELEASE:
                newFragment = new NewProductReleaseFragment();
                break;
            default:
                break;
        }

        if (newFragment != null) {

            boolean animateTransition = true;
            boolean isBackTransition = false;

            if (args != null) {
                newFragment.setArguments(args);
                animateTransition = args.getBoolean(UiUpdateHandler.ARG_REPLACE_FRAGMENT_WITH_ANIMATION, true);
                isBackTransition = args.getBoolean(UiUpdateHandler.ARG_IS_BACK_TRANSITION, false);
            }

            FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();

            if (animateTransition) {
                if (isBackTransition) {
                    transaction.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
                } else {
                    transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
                }
            }

            transaction.replace(R.id.layoutFragmentContainer, newFragment).commit();

            // Only add the current fragment into the stack when navigation is going forward.
            if (!isBackTransition) {
                if (mCurrentFragment >= 0) { // Skip the invalid initialized value of mCurrentFragment.
                    mNavigationStack.add(mCurrentFragment);
                }
            } else {
                // Remove the last fragment from the navigation stack when going back.
                mNavigationStack.remove(mNavigationStack.size() - 1);
            }

            mCurrentFragment = fragment;
        }
    }

    /**
     * Sets listeners for navigation drawer textviews.
     * @param navItem The item to set listeners for.
     * @param linkItemTo The fragment that the item should navigate to.
     */
    private void setNavItemListeners(TextView navItem, int linkItemTo) {
        // Set the listeners
        navItem.setOnClickListener(new NavItemOnClickListener(linkItemTo));
        navItem.setOnTouchListener(new NavItemOnTouchListener());

        // And add the TextView to the list of navigation drawer text views
        mNavigationTextViews.add(navItem);
    }

    /**
     * Navigation drawer link items have a indicator ("[ ]" / [x]")
     * that tells which navigation item is currently active.
     * This method clears all the navigation items back to "[ ]" state.
     */
    private void clearNavItemIndicators() {
        for (TextView tw : mNavigationTextViews) {
            setNavItemIndicator(tw, false);
        }
    }

    /**
     * Sets the selected nav item indicator for a text view either on or off.
     *
     * @param tw Navigation item TextView
     * @param selected True marks this item selected ("[x]")
     */
    private void setNavItemIndicator(TextView tw, boolean selected) {
        String textNow = tw.getText().toString();
        String newToggle = "[" + (selected ? "x" : " ") + "]";
        tw.setText(textNow.replace(textNow.substring(0, 3), newToggle));
    }

    /**
     * OnTouchListener for navigation drawer items.
     * The listener highlights the item when it is touched.
     */
    private class NavItemOnTouchListener implements View.OnTouchListener {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            // These are TextViews.
            TextView tv = (TextView) v;

            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    tv.setTextColor(ContextCompat.getColor(MainActivity.this, R.color.accent));
                    break;
                default:
                    tv.setTextColor(Color.WHITE);
            }
            return false;
        }
    }

    /**
     * OnClickListener for navigation drawer items.
     * Clicking an item changes the currently active fragment visible
     * on the MainActivity.
     */
    private class NavItemOnClickListener implements View.OnClickListener {
        private int mLinksToFragment; // The fragment this items links to.

        NavItemOnClickListener(int linksTo) {
            mLinksToFragment = linksTo;
        }

        @Override
        public void onClick(View v) {
            updateUi(Integer.MIN_VALUE, null);
            switchFragment(mLinksToFragment, null);
            mDrawerLayout.closeDrawer(GravityCompat.START);

            // Navigation stack is emptied when a new fragment is entered from
            // the navigation drawer.
            mNavigationStack.clear();

            clearNavItemIndicators();
            setNavItemIndicator((TextView) v, true);
        }
    }

    @Override
    public void updateUi(int action, Bundle args) {
        //Log.d(TAG, "updateUi() - start");

        // First change the fragment if a fragment change is requested.
        if (action == UiUpdateHandler.ACTION_REPLACE_FRAGMENT) {
            switchFragment(args.getInt(UiUpdateHandler.ARG_TARGET_FRAGMENT), args);
        } else if (action == UiUpdateHandler.ACTION_GO_BACK) { // Or navigate back on the navigation stack.
            goBackOnNavigationStack();
        }

        // Then update the top bar
        Company company = GameEngine.getInstance().getGameState().getCompany();
        TextView companyName = (TextView) findViewById(R.id.textViewCompanyName);
        TextView money = (TextView) findViewById(R.id.textViewMoney);
        TextView moneyRate = (TextView) findViewById(R.id.textViewNavMoneyRate);
        TextView codeRate = (TextView) findViewById(R.id.textViewNavCodeRate);

        Resources res = AppTycoonApp.getContext().getResources();
        String strMoney = res.getString(R.string.top_bar_money,
                Utils.largeNumberToNiceString(company.getFunds(), 2));

        companyName.setText(company.getName());
        money.setText(strMoney);

        moneyRate.setText(Utils.largeNumberToNiceString(company.getIncome() - company.getRunningCosts(), 2));
        codeRate.setText(Utils.largeNumberToNiceString(company.getCps(), 2));

    }

    /**
     * Switches the current fragment back to the previous one on the stack.
     *
     * If the currently visible fragment is the only one in the stack, then this
     * method does nothing.
     *
     * NOTE! Back navigation with custom arguments do not go through this method, but directly
     * to switchFragment!
     */
    private void goBackOnNavigationStack() {
        if (mNavigationStack.size() <= 0) {
            Log.d(TAG, "goBackOnNavigationStack() - already at the bottom");
        } else {
            Log.d(TAG, "goBackOnNavigationStack() - going back");
            int prevFragment = mNavigationStack.get(mNavigationStack.size() - 1);
            Bundle args = new Bundle();
            args.putBoolean(UiUpdateHandler.ARG_IS_BACK_TRANSITION, true);
            switchFragment(prevFragment, args);
        }
    }

    /**
     * Prompts the user for quitting the app. Quits on no.
     */
    private void promptExit() {
        final AppTycoonAlertDialog dialog = new AppTycoonAlertDialog(this, "Exit", "Are you sure?");
        dialog.setOkAction("Yes", new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dialog.dismiss();
                finish();
            }
        });
        dialog.setCancelAction("No", new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dialog.dismiss();
            }
        });
        dialog.show();
    }

    /**
     * Override back button to prevent accidental app exits. This because everything is now
     * running on one activity. So, without this, a single back press would kill the entire app.
     * TODO: Make back button to navigate one step back when there is a back action set for current fragment.
     */
    @Override
    public void onBackPressed() {
        Log.d(TAG, "onBackPressed() - navstack size: " + mNavigationStack.size());

        // If the navigation stack is empty, prompt for exit.
        if (mNavigationStack.size() > 0) {
            goBackOnNavigationStack();
        } else {
            promptExit();
        }
    }
} public class UiUpdateHandler extends Handler {

    private static final String TAG = "UiUpdateHandler";

    //
    //
    // Update actions that UiUpdaters implement.

    /** Replaces the currently visible fragment with another one. */
    public static final int ACTION_REPLACE_FRAGMENT = 12;

    /** Refreshes the UI. This calls the updateUi() of all registered UiUpdaters. */
    public static final int ACTION_REFRESH_UI = 20;

    /** Navigates one step back in the navigation stack. */
    public static final int ACTION_GO_BACK = 30;

    //
    //
    // Constants for argument Bundle keys.

    /** ID of the fragment to navigate to. Type: int */
    public static final String ARG_TARGET_FRAGMENT = "target_fragment";

    /** Flag to toggle fragment transition animation of/off.
     * This is optional and defaults to true. Type: boolean */
    public static final String ARG_REPLACE_FRAGMENT_WITH_ANIMATION = "replace_fragment_anim";

    /** Flag for telling the new fragment is a previous one in the navigation stack.
     * Type: boolean */
    public static final String ARG_IS_BACK_TRANSITION = "override_back_animation";

    /** Used with project slot related fragments. The array index of the project slot.
     * Type: int */
    public static final String ARG_PROJECT_SLOT_INDEX = "project_slot_index";

    /** Used with product related fragments. The ID of the product type. Type: int */
    public static final String ARG_NEW_PRODUCT_TYPE = "product_type";

    /** Used to pass a product to a fragment. This should be an index of the product in the
     * List of products in the Company object. */
    public static final String ARG_PRODUCT_INDEX = "product_index";

    // List of currently registered UiUpdaters and their actions.
    private Map<Integer, List<UiUpdater>> mUpdaters;

    private static UiUpdateHandler sInstance;

    private UiUpdateHandler() {
        super(Looper.getMainLooper());
        mUpdaters = new HashMap<>();
    }

    public static synchronized UiUpdateHandler getInstance() {
        if (sInstance == null) {
            sInstance = new UiUpdateHandler();
        }
        return sInstance;
    }

    /**
     * Register a class to handle an action.
     *
     * @param updater UiUpdater
     * @param action Action to implement.
     */
    public void registerUpdater(UiUpdater updater, int action) {
        Log.d(TAG, "registerUpdate() - registering action " + action + " for class " + updater.getClass().getName());
        List<UiUpdater> updaters = mUpdaters.get(action);

        if (updaters == null) {
            updaters = new ArrayList<>();
            mUpdaters.put(action, updaters);
        }

        updaters.add(updater);
    }

    /**
     * Unregisters a class from handling actions. After unregistering,
     * actions that were assigned to this UiUpdater are ignored.
     *
     * @param updater UiUpdater to unregister.
     */
    public void unRegisterUpdater(UiUpdater updater) {
        Log.d(TAG, "unregisterUpdater() - unregistering " + updater.getClass().getName());
        for (int action : mUpdaters.keySet()) {
            List<UiUpdater> updaters = mUpdaters.get(action);

            Log.d(TAG, "unRegisterUpdater() - size before remove = " + updaters.size());
            updaters.remove(updater);
            Log.d(TAG, "unRegisterUpdater() - size after remove = " + updaters.size());

            Log.d(TAG, "unregisterUpdater() - unregistered action " + action);
        }
    }

    /**
     * Unregisters all updaters for a specific action.
     *
     * @param action ID of the action to unregister UiUpdaters for.
     */
    public void unRegisterAllForAction(int action) {
        mUpdaters.remove(action);
    }


    @Override
    public void handleMessage(Message msg) {
        //Log.d(TAG, "handleMessage() - what  = " + msg.what);

        if (!mUpdaters.containsKey(msg.what)) {
            Log.d(TAG, "handleMessage() - no UiUpdater assigned -> do nothing");
            return;
        }

        for (UiUpdater updater : mUpdaters.get(msg.what)) {
            updater.updateUi(msg.what, msg.getData());
        }

    }

    /**
     * Generates a message which after sending changes the current fragment.
     *
     * @param targetFragment Fragment ID to change to.
     * @return The message
     */
    public static Message obtainReplaceFragmentMessage(int targetFragment) {
        Message msg = getInstance().obtainMessage(UiUpdateHandler.ACTION_REPLACE_FRAGMENT);
        Bundle data = new Bundle();
        data.putInt(UiUpdateHandler.ARG_TARGET_FRAGMENT, targetFragment);
        msg.setData(data);
        return msg;
    }

    /**
     * Generates a message which after sending navigates one step back on the navigation
     * stack.
     *
     * @return The message
     */
    public static Message obtainGoBackMessage() {
        Message msg = getInstance().obtainMessage(UiUpdateHandler.ACTION_GO_BACK);
        return msg;
    }

    public static Message obtainUiRefreshMessage() {
        Message msg = getInstance().obtainMessage(ACTION_REFRESH_UI);
        return msg;
    }
} public class AddProductFeatureDialog extends AppTycoonDialog {

    private static final String TAG = "AddProductFeatureDialog";

    private Product mProduct;
    private CustomCallback mOnFeatureSelectedCallback;

    /**
     * @param product Product the feature is being added to
     * @param context Activity context
     * @param onFeatureSelected Callback that's called when a new feature is added
     */
    public AddProductFeatureDialog(Product product,
                                   Context context,
                                   CustomCallback onFeatureSelected) {
        super(context,
                R.layout.dialog_add_product_feature,
                "Add a feature");

        mProduct = product;
        mOnFeatureSelectedCallback = onFeatureSelected;

        setCancelAction(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerViewAddableFeatures);
        List<ProductFeature> addableFeatures = mProduct.getType().getPossibleFeatures();
        AddableFeaturesRecyclerViewAdapter adapter = new AddableFeaturesRecyclerViewAdapter(addableFeatures, this);

        Log.d(TAG, "showAddFeatureDialog() - " + addableFeatures.size() + " features");
        recyclerView.setLayoutManager(new LinearLayoutManager(context));
        recyclerView.setAdapter(adapter);
    }


    /**
     * A RecyclerViewAdapter for the features that can be added to the product.
     */
    private class AddableFeaturesRecyclerViewAdapter extends RecyclerView.Adapter<AddableFeaturesRecyclerViewAdapter.ViewHolder> {
        private List<ProductFeature> mFeatures;
        private AppTycoonDialog mAddFeatureDialog;

        AddableFeaturesRecyclerViewAdapter(List<ProductFeature> features, AppTycoonDialog dialog) {
            mFeatures = features;
            mAddFeatureDialog = dialog;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_addable_product_feature, parent, false);
            ViewHolder viewHolder = new ViewHolder(view);
            viewHolder.textViewFeatureName = (TextView) view.findViewById(R.id.textViewFeatureName);
            viewHolder.parent = view;

            return viewHolder;
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            final ProductFeature feature = mFeatures.get(position);
            holder.textViewFeatureName.setText(feature.getName());

            // Clicking a feature adds the feature for the product.
            //
            // The feature is always with its level set to 1.
            holder.textViewFeatureName.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mProduct.addFeature(feature, 1);
                    mOnFeatureSelectedCallback.callBack();
                    mAddFeatureDialog.dismiss();
                }
            });

            holder.textViewFeatureName.setOnTouchListener(new TextViewChangeColourOnTouchListener(Color.GRAY,
                    holder.textViewFeatureName.getCurrentTextColor()));
        }

        @Override
        public int getItemCount() {
            return mFeatures.size();
        }

        class ViewHolder extends RecyclerView.ViewHolder {
            View parent; // The top level layout view of the item.
            TextView textViewFeatureName;

            ViewHolder(View itemView) {
                super(itemView);
            }
        }
    }
} public class AppTycoonDialog extends Dialog {

    private TextView mTextViewOk;
    private TextView mTextViewCancel;

    /**
     * Constructor.
     *
     * @param context Current activity context.
     * @param layout ID of the content layout resource for the dialog.
     * @param dialogTitle Title of the dialog.
     */
    public AppTycoonDialog(Context context,
                           int layout,
                           String dialogTitle) {
        this(context, LayoutInflater.from(context).inflate(layout, null), dialogTitle);
    }

    /**
     * Constructor.
     *
     * @param context Current activity context.
     * @param contentView View to be placed as the content of the dialog.
     * @param dialogTitle Title of the dialog.
     */
    public AppTycoonDialog(Context context,
                           View contentView,
                           String dialogTitle) {
        super(context);
        setContentView(R.layout.dialog_apptycoon_dialog);

        RelativeLayout layoutContentContainer = (RelativeLayout) findViewById(R.id.layoutDialogContent);
        layoutContentContainer.addView(contentView);

        setTitle(dialogTitle);

        // Action buttons are hidden until an action for them is set.
        mTextViewOk = (TextView) findViewById(R.id.textViewDialogActionOk);
        mTextViewCancel = (TextView) findViewById(R.id.textViewDialogActionCancel);
        mTextViewOk.setVisibility(View.INVISIBLE);
        mTextViewCancel.setVisibility(View.INVISIBLE);
    }

    /**
     * Sets the title for the dialog.
     *
     * @param title New title for the dialog.
     */
    @Override
    public void setTitle(@Nullable CharSequence title) {
        ((TextView) findViewById(R.id.textViewDialogTitle)).setText(title);
    }

    private void setActionTextViewListener(TextView view, View.OnClickListener listener) {
        view.setOnTouchListener(new TextViewChangeColourOnTouchListener(Color.BLACK,
                view.getCurrentTextColor()));
        view.setOnClickListener(listener);
    }

    public void setOkAction(View.OnClickListener onOkListener) {
        mTextViewOk.setVisibility(View.VISIBLE);
        setActionTextViewListener(mTextViewOk, onOkListener);
    }

    public void setCancelAction(View.OnClickListener onCancelListener) {
        mTextViewCancel.setVisibility(View.VISIBLE);
        setActionTextViewListener(mTextViewCancel, onCancelListener);
    }

    public void setOkLabel(String label) {
        mTextViewOk.setText(label);
    }

    public void setCancelLabel(String label) {
        mTextViewCancel.setText(label);
    }

    public void setOkAction(String label, View.OnClickListener onOkListener) {
        setOkAction(onOkListener);
        setOkLabel(label);
    }

    public void setCancelAction(String label, View.OnClickListener onCancelListener) {
        setCancelAction(onCancelListener);
        setCancelLabel(label);
    }

    /**
     * A custom call back meant to be used on dialogs that extend the AppTycoonDialog.
     */
    public interface CustomCallback {
        void callBack();
    }

} public class ChangeProductNameDialog extends AppTycoonDialog {

    /**
     * @param product Product that's being edited
     * @param context Activity context
     * @param onNameChanged Callback that's called after the name has been changed
     */
    public ChangeProductNameDialog(final Product product, Context context, final CustomCallback onNameChanged) {
        super(context,
                R.layout.dialog_change_product_name,
                "Set product name");
        final EditText editTextName = (EditText) findViewById(R.id.editTextName);
        editTextName.setText(product.getName());
        setOkAction(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                product.setName(editTextName.getText().toString());
                onNameChanged.callBack();
                dismiss();
            }
        });
    }
} public class EditProductFeatureDialog extends AppTycoonDialog {

    /**
     * @param product Product that the feature belongs to
     * @param feature Feature that's being edited
     * @param context Activity context
     * @param onFeatureChanged Callback that's called when the feature is changed
     */
    public EditProductFeatureDialog(final Product product,
                                    final ProductFeature feature,
                                    Context context,
                                    final CustomCallback onFeatureChanged) {
        super(context,
                R.layout.dialog_edit_product_feature,
                "Edit feature");

        final EditText editTextLevel = (EditText) findViewById(R.id.editTextFeatureLevel);
        String stringCurrentLevel = "" + product.getLevelOfAFeature(feature);
        editTextLevel.setText(stringCurrentLevel);

        TextView textView = (TextView) findViewById(R.id.textViewFeatureName);
        textView.setText(feature.getName());

        setOkAction(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                product.addFeature(feature, Integer.parseInt(editTextLevel.getText().toString()));
                onFeatureChanged.callBack();
                dismiss();
            }
        });
    }
} public class FragmentBackButtonOnClickListener implements View.OnClickListener {
    private static final String TAG = "FBBOCL";

    private Bundle mArgs;
    private int mPreviousFragment;

    /**
     * Use this to simply navigate one step back on the navigation stack.
     */
    public FragmentBackButtonOnClickListener() {

    }

    /**
     * Use this to navigate back AND pass a custom set of arguments with the transition to
     * the target fragment.
     *
     * @param previousFragment ID of the fragment to navigate to
     * @param args Custom set of arguments to pass with the transition
     */
    public FragmentBackButtonOnClickListener(int previousFragment, Bundle args) {
        mPreviousFragment = previousFragment;
        mArgs = args;
    }

    @Override
    public void onClick(View v) {
        boolean customArgs = mArgs != null;

        Log.d(TAG, "onClick() - custom args set: " + customArgs);

        // If custom args are set, treat the transition as a 'regular' fragment transition.
        Message msg = customArgs ? UiUpdateHandler.obtainReplaceFragmentMessage(mPreviousFragment) :
                                    UiUpdateHandler.obtainGoBackMessage();

        if (customArgs) {
            // Raise the flag telling that this 'regular' transition is actually a back transition.
            mArgs.putBoolean(UiUpdateHandler.ARG_IS_BACK_TRANSITION, true);
            mArgs.putAll(msg.getData()); // Add all the original args as well, as e.g. the target
                                        // fragment ID is set there.
            msg.setData(mArgs);
        }

        msg.sendToTarget();
    }
} public class TextViewChangeColourOnTouchListener implements View.OnTouchListener {

    private int mOnTouchColor;
    private int mOriginalColor;

    /**
     * Constructor.
     *
     * Parameter colors are {@link android.graphics.Color Color} colors.
     *
     * @param onTouchColor Color of the text when touched.
     * @param originalColor Original color of the text.
     */
    public TextViewChangeColourOnTouchListener(int onTouchColor, int originalColor) {
        mOnTouchColor = onTouchColor;
        mOriginalColor = originalColor;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // The view is ALWAYS a TextViews.
        TextView tv = (TextView) v;

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                tv.setTextColor(mOnTouchColor);
                break;
            default:
                tv.setTextColor(mOriginalColor);
        }
        return false;
    }
} public class Utils {

    private static final String TAG = "Utils";

    /**
     * Selects and returns a random entry from a list.
     * @param list List to select an entry from.
     * @return
     */
    public static Object getRandomFromList(List list) {
        Random r = new Random();
        return list.get(r.nextInt(list.size()));
    }


    public static String millisecondsToTimeString(long milliseconds) {
        // TODO: Custom formatting
        double minutes = Math.floor(milliseconds / 60000);

        double seconds = (double) (milliseconds - minutes * 60000) / 1000;
        seconds = Math.floor(seconds);

        String ret = String.format("%.0fm %.0fs", minutes, seconds);

        return ret;
    }

    /**
     * Takes a large number and prints with a 'power of 1000' suffix added. For example,
     * 12489 becomes 12.49k (with 2 decimals). Numbers less smaller than 1000 will be shown with
     * the specified number of decimals, without a character suffix.
     *
     * @param numberToConvert The large number to convert.
     * @param decimals Number of decimals to show.
     * @return A nicely formatted string presentation of the large number.
     */
    public static String largeNumberToNiceString(double numberToConvert, int decimals) {
        // If the number is large enough for the smallest 'power of 1000', then just
        // use the implementation for numbers of type long.
        if (numberToConvert >= 1000.00d) {
            return largeNumberToNiceString((long) numberToConvert, decimals);
        }

        // Otherwise return the number with desired number of decimals as a string.
        return String.format("%." + decimals + "f", numberToConvert);
    }

    /**
     * Takes a large number and prints with a 'power of 1000' suffix added. For example,
     * 12489 becomes 12.49k (with 2 decimals).
     *
     * @param numberToConvert The large number to print.
     * @param decimals Number of decimals to show.
     * @return A nicely formatted string presentation of the large number.
     */
    public static String largeNumberToNiceString(long numberToConvert, int decimals) {
        String[] suffixes = {"k", "M", "G", "T", "P"};
        String suffix = "";
        double number = 0.0;

        // get a possible "power of 1000" prefix for the number
        for (int i = suffixes.length - 1; i >= 0; i--) {
            if (numberToConvert / Math.pow(1000, i + 1) > 1) {
                number = numberToConvert / Math.pow(1000, i + 1);
                suffix = suffixes[i];
                break;
            }
        }

        //Log.d(TAG, "largeNumberToNiceString() - numberToConvert = " + numberToConvert);
        //Log.d(TAG, "largeNumberToNiceString() - number = " + number);

        // If no "power of 1000" was not found, just use the numberToConvert directly.
        // This happens if the number is less than 1000.
        if (number <= 0.0f) {
            return Long.toString(numberToConvert);
        }

        double residue = number - Math.floor(number);
        residue = residue * Math.pow(10, decimals);
        residue = Math.round(residue) / Math.pow(10, decimals);
        number = Math.floor(number) + residue;

        return String.format("%." + decimals + "f", number) + suffix;
    }


    private static List<String> sFirstNames;
    private static List<String> sLastNames;

    /**
     * Generates a random name from seeds in /res/raw/firstnames.txt and lastnames.txt
     * @return A random name.
     */
    public static String getRandomName() {
        Context context = AppTycoonApp.getContext();

        // Load the seeds if not already loaded.
        if (sFirstNames == null || sLastNames == null) {
            sFirstNames = new ArrayList<>();
            sLastNames = new ArrayList<>();

            // First names
            InputStream in = context.getResources().openRawResource(R.raw.firstnames);
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line = null;
            try {
                line = br.readLine();
                while (line != null) {
                    if (line.length() > 1) {
                        sFirstNames.add(line);
                    }
                    line = br.readLine();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            // Last names
            in = context.getResources().openRawResource(R.raw.lastnames);
            br = new BufferedReader(new InputStreamReader(in));
            line = null;
            try {
                line = br.readLine();
                while (line != null) {
                    if (line.length() > 1) {
                        sLastNames.add(line);
                    }
                    line = br.readLine();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        String ret = getRandomFromList(sFirstNames) + " " + getRandomFromList(sLastNames);
        Log.d(TAG, "new random name: " + ret);
        return ret;
    }

    public static String getRandomCustomer() {


        return "Random Customer Inc.";
    }


}
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.util.Pair;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.List;
import jaakko.jaaska.apptycoon.AppTycoonApp;
import jaakko.jaaska.apptycoon.R;
import jaakko.jaaska.apptycoon.engine.core.GameEngine;
import jaakko.jaaska.apptycoon.engine.product.Product;
import jaakko.jaaska.apptycoon.engine.product.ProductFeature;
import jaakko.jaaska.apptycoon.engine.product.ProductType;
import jaakko.jaaska.apptycoon.engine.project.ProductDevelopmentProject;
import jaakko.jaaska.apptycoon.ui.MainActivity;
import jaakko.jaaska.apptycoon.ui.UiUpdateHandler;
import jaakko.jaaska.apptycoon.ui.dialog.AddProductFeatureDialog;
import jaakko.jaaska.apptycoon.ui.dialog.AppTycoonDialog;
import jaakko.jaaska.apptycoon.ui.dialog.ChangeProductNameDialog;
import jaakko.jaaska.apptycoon.ui.dialog.EditProductFeatureDialog;
import jaakko.jaaska.apptycoon.ui.listener.FragmentBackButtonOnClickListener;
import jaakko.jaaska.apptycoon.ui.listener.TextViewChangeColourOnTouchListener;
import jaakko.jaaska.apptycoon.utils.Utils;
package jaakko.jaaska.apptycoon.ui.fragment;




/**
 * A fragment for configuring a new product.
 */

public class NewProductFragment extends Fragment {

    private static final String TAG = "NewProductFragment";

    /** The parent layout view of the fragment. */
    private View mView;

    private ProductFeaturesRecyclerViewAdapter mRecyclerViewAdapter;

    /** The new product being configures. */
    private Product mProduct;

    /** The project for the initial development of this product. */
    private ProductDevelopmentProject mProject;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        mView = inflater.inflate(R.layout.fragment_new_product, container, false);

        int productTypeId = getArguments().getInt(UiUpdateHandler.ARG_NEW_PRODUCT_TYPE);
        ProductType productType = ProductType.getProductType(productTypeId);

        // Init the new product and the project for its initial development.
        Log.d(TAG, "onCreateView() - type of new product is '" + productType.getName() + "'");
        mProduct = new Product("_unnamed_", productType);
        mProduct.rebuildNewProductDevelopmentProject();
        mProduct.setDevelopmentProject(mProject);

        // Setup the fragment top bar title and action button actions.
        TextView viewBack = (TextView) mView.findViewById(R.id.textViewBack);
        viewBack.setOnClickListener(new FragmentBackButtonOnClickListener());
        viewBack.setOnTouchListener(new TextViewChangeColourOnTouchListener(Color.BLACK, viewBack.getCurrentTextColor()));

        TextView textViewTitle = (TextView) mView.findViewById(R.id.textViewTitle);
        textViewTitle.setText("New " + productType.getName());

        // "Done" action
        TextView viewDone = (TextView) mView.findViewById(R.id.textViewAction);
        viewDone.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Add the product to Company's list of products.
                GameEngine.getInstance().getGameState().getCompany().addProduct(mProduct);

                // Go back to "Product Development" fragment
                UiUpdateHandler.obtainReplaceFragmentMessage(MainActivity.FRAGMENT_PRODUCTS)
                        .sendToTarget();
            }
        });
        viewDone.setOnTouchListener(new TextViewChangeColourOnTouchListener(Color.BLACK, viewDone.getCurrentTextColor()));

        // Setup the RecyclerView for the product features.
        RecyclerView recyclerView = (RecyclerView) mView.findViewById(R.id.recyclerViewProductFeatures);
        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        recyclerView.setHasFixedSize(true);
        mRecyclerViewAdapter = new ProductFeaturesRecyclerViewAdapter();
        recyclerView.setAdapter(mRecyclerViewAdapter);

        // Change name action button.
        TextView viewChangeName = (TextView) mView.findViewById(R.id.textViewChangeNameAction);
        viewChangeName.setOnTouchListener(new TextViewChangeColourOnTouchListener(Color.BLACK,
                viewChangeName.getCurrentTextColor()));
        viewChangeName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showNameChangeDialog();
            }
        });

        // Add feature action button
        TextView viewAddFeature = (TextView) mView.findViewById(R.id.textViewAddFeatureAction);
        viewAddFeature.setOnTouchListener(new TextViewChangeColourOnTouchListener(Color.BLACK,
                viewAddFeature.getCurrentTextColor()));
        viewAddFeature.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showAddFeatureDialog();
            }
        });

        refreshViewsAndNewProductProject();

        return mView;
    }

    /**
     * Call this when the data for the new product has changed to update
     * the changes into the views and also refresh the new product development
     * project.
     */
    private void refreshViewsAndNewProductProject() {
        mProduct.rebuildNewProductDevelopmentProject();

        TextView textViewName = (TextView) mView.findViewById(R.id.textViewProductName);
        TextView textViewComplexity = (TextView) mView.findViewById(R.id.textViewProductComplexity);
        TextView textViewWorkAmount = (TextView) mView.findViewById(R.id.textViewProductDevWorkAmount);

        textViewName.setText(mProduct.getName());
        textViewComplexity.setText(Utils.largeNumberToNiceString(mProduct.getComplexity(), 2));
        textViewWorkAmount.setText(Utils.largeNumberToNiceString(mProduct.getDevelopmentProject().getWorkAmount(), 2));
    }

    /**
     * Builds and shows a dialog for changing the product name.
     */
    private void showNameChangeDialog() {
        new ChangeProductNameDialog(mProduct,
                getActivity(),
                new AppTycoonDialog.CustomCallback() {
                    @Override
                    public void callBack() {
                        refreshViewsAndNewProductProject();
                    }
                }).show();
    }

    /**
     * Build and show a dialog for editing (i.e. setting the feature level) a feature.
     *
     * @param feature The ProductFeature to edit with the dialog.
     */
    private void showEditFeatureDialog(final ProductFeature feature) {
        new EditProductFeatureDialog(mProduct, feature, getActivity(),
                new AppTycoonDialog.CustomCallback() {
                    @Override
                    public void callBack() {
                        mRecyclerViewAdapter.notifyDataSetChanged();
                        refreshViewsAndNewProductProject();
                    }
        }).show();
    }

    /**
     * Show a dialog for adding a feature into the product.
     */
    private void showAddFeatureDialog() {
        new AddProductFeatureDialog(mProduct,