/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package org.rhq.core.domain.shared;

import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.ResourceAvailability;
import org.rhq.core.domain.resource.InventoryStatus;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceCategory;
import org.rhq.core.domain.resource.ResourceType;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * ResoureBuilder is a builder object that creates Resource objects. The builder ensures that a Resource is created
 * in a valid state, specifically, fields that are not nullable are required to have non-null values. Using a builder
 * should help make the intent of tests clearer and more self-documenting.
 * <br/><br/>
 * Note that this class currently does not yet provide support for all Resource fields/properties. 
 *
 * @author John Sanda
 */
public class ResourceBuilder {

    private ResourceBuilder parentBuilder;

    private Resource resource;

    private Random random;

    private boolean useDefaultResourceType;

    private ResourceCategory category;

    private List<ResourceBuilder> childBuilders = new ArrayList<ResourceBuilder>();

    public static class AssociationBuilder {
        private ResourceBuilder resourceBuilder;

        private int count;

        AssociationBuilder(ResourceBuilder builder, int count) {
            resourceBuilder = builder;
            this.count = count;
        }

        public ResourceBuilder randomChildServers() {
            for (int i = 0; i < count; ++i) {
                ResourceBuilder childBuilder = new ResourceBuilder(ResourceCategory.SERVER, resourceBuilder);
                resourceBuilder.childBuilders.add(childBuilder.createRandomServer());
            }
            return resourceBuilder;
        }

        public ResourceBuilder randomChildServices() {
            for (int i = 0; i < count; ++i) {
                ResourceBuilder childBuilder = new ResourceBuilder(ResourceCategory.SERVICE, resourceBuilder);
                resourceBuilder.childBuilders.add(childBuilder.createRandomService());
            }
            return resourceBuilder;
        }
    }

    public static class ChildrenResourceBuilder {
        private ResourceBuilder parentBuilder;

        ChildrenResourceBuilder(ResourceBuilder builder, ResourceCategory category, int numChildren) {
            parentBuilder = builder;
            parentBuilder.childBuilders = new ArrayList<ResourceBuilder>(numChildren);
            for (int i = 0; i < numChildren; ++i) {
                parentBuilder.childBuilders.add(new ResourceBuilder(category, parentBuilder).createResource());
            }
        }

        public ChildrenResourceBuilder inInventory() {
            for (ResourceBuilder childBuilder : parentBuilder.childBuilders) {
                childBuilder.inInventory();
            }
            return this;
        }

        public ChildrenResourceBuilder notInInventory() {
            for (ResourceBuilder childBuilder : parentBuilder.childBuilders) {
                childBuilder.notInInventory();
            }
            return this;
        }

        public ResourceBuilder included() {
            return parentBuilder;
        }
    }


    public ResourceBuilder() {
    }

    private ResourceBuilder(ResourceCategory category, ResourceBuilder parentBuilder) {
        this.category = category;
        this.parentBuilder = parentBuilder;
    }

    public ResourceBuilder createResource() {
        resource = new Resource();
        random = new Random();
        return this;
    }

    public ResourceBuilder createPlatform() {
        category = ResourceCategory.PLATFORM;
        return createResource();
    }

    public ResourceBuilder createServer() {
        category = ResourceCategory.SERVER;
        return createResource();
    }

    public ResourceBuilder createRandomServer() {
        category = ResourceCategory.SERVER;
        createResource();
        withRandomId();
        withRandomName("server:");
        withRandomResourceKey("server:");
        withRandomUuid("server:");
        withDefaultServerResourceType();

        return this;
    }

    public ResourceBuilder createService() {
        category = ResourceCategory.SERVICE;
        return createResource();
    }

    public ResourceBuilder createRandomService() {
        category = ResourceCategory.SERVICE;
        createResource();
        withRandomId();
        withRandomName("service:");
        withRandomResourceKey("service:");
        withRandomUuid("service:");
        withDefaultServiceResourceType();

        return this;
    }

    /**
     * Using a default resource type results in <code>Resource.resourceType</code> being assigned to a new
     * ResourceType object that has some default values applied to it. If the Resource being created is a platform, then
     * the ResourceType will be a platform. More specifically, <code>ResourceType.category</code> will be assigned a
     * value of {@link ResourceCategory#PLATFORM}. Likewise, if the Resource being created is a server, then its
     * ResourceType object will have a category of {@link ResourceCategory#SERVER}. And if the Resource is a service,
     * then the ResourceType category will be {@link ResourceCategory#SERVICE}.
     * <br/><br/>
     * The resource type name defaults to the name of resource. And the plugin name (as specified by
     * ResourceType.plugin) defaults to <code>Resource.name + " Plugin"</code>
     * <br/><br/>
     * When using a default resource type, the resource must be created using one of {@link #createPlatform()},
     * {@link #createServer()}, or {@link #createService()}; otherwise, an exception will be thrown since the builder
     * will not have sufficient information to create the resource type.
     * <br/><br/>
     * Lastly, if you specify that a default resource type by calling this method and also specify the resource tye
     * with {@link #withResourceType(org.rhq.core.domain.resource.ResourceType)}, the latter will be overwritten regardless of when it is called. The
     * default will be used instead.
     *
     * @return The builder
     *
     */
    public ResourceBuilder usingDefaultResourceType() {
        useDefaultResourceType = true;
        return this;
    }

    public ResourceBuilder withId(int id) {
        resource.setId(id);
        return this;
    }

    public ResourceBuilder withRandomId() {
        resource.setId(random.nextInt());
        return this;
    }

    public ResourceBuilder withResourceKey(String key) {
        resource.setResourceKey(key);
        return this;
    }

    public ResourceBuilder withRandomResourceKey(String prefix) {
        resource.setResourceKey(prefix + randomString());
        return this;
    }

    public ResourceBuilder withRandomResourceKey() {
        return withRandomResourceKey("");
    }

    public ResourceBuilder withName(String name) {
        resource.setName(name);
        return this;
    }

    public ResourceBuilder withRandomName(String prefix) {
        resource.setName(prefix + randomString());
        return this;
    }

    public ResourceBuilder withRandomName() {
        return withRandomName("");
    }

    public ResourceBuilder withResourceType(ResourceType resourceType) {
        resource.setResourceType(resourceType);
        return this;
    }

    public ResourceBuilder withUuid(String uuid) {
        resource.setUuid(uuid);
        return this;
    }

    public ResourceBuilder withRandomUuid(String prefix) {
        resource.setUuid(prefix + randomString());
        return this;
    }

    public ResourceBuilder withRandomUuid() {
        return withRandomUuid("");
    }

    public ResourceBuilder withVersion(String version) {
        resource.setVersion(version);
        return this;
    }

    public ResourceBuilder withCurrentAvailability(AvailabilityType availabilityType) {
        ResourceAvailability availability = new ResourceAvailability(resource,  availabilityType);
        resource.setCurrentAvailability(availability);
        return this;
    }

    public ResourceBuilder withInventoryStatus(InventoryStatus inventoryStatus) {
        resource.setInventoryStatus(inventoryStatus);
        return this;
    }

    /**
     * Set the <code>inventoryStatus</code> to {@link InventoryStatus#COMMITTED}
     *
     * @return The builder
     */
    public ResourceBuilder inInventory() {
        resource.setInventoryStatus(InventoryStatus.COMMITTED);
        return this;
    }

    /**
     * Set the <code>inventoryStatus</code> to {@link InventoryStatus#NEW}
     *
     * @return The builder
     */
    public ResourceBuilder notInInventory() {
        resource.setInventoryStatus(InventoryStatus.NEW);
        return this;
    }

    public AssociationBuilder with(int count) {
        return new AssociationBuilder(this, count);
    }

    public ResourceBuilder withChildService() {
        ResourceBuilder childBuilder = new ResourceBuilder(ResourceCategory.SERVICE, this);
        childBuilders.add(childBuilder.createService());
        return childBuilder;
    }

    public ResourceBuilder included() {
        return this.parentBuilder;
    }

    public Resource build() {
        String errors = validate();
        if (errors != null) {
            throw new BuilderException(errors);
        }

        if (useDefaultResourceType) {
            withDefaultResourceType();
        }

        for (ResourceBuilder childBuilder : childBuilders) {
            resource.addChildResource(childBuilder.build());
        }

        return resource;
    }

    private String validate() {
        StringBuilder errors = new StringBuilder();

        if (resource.getUuid() == null) {
            // Making uuid required since it is used in equals/hashCode
            errors.append("uuid is a required property\n");
        }

        if (resource.getName() == null) {
            errors.append("name is a required property\n");
        }

        if (useDefaultResourceType && category == null) {
            errors.append("When using default resource type, the resource must be created with one of " +
                "createPlatform(), createServer(), or createService()\n");
        }

        // We only care that resourceType is set if we are not using a default type. If we are using a default,
        // then the resourceType property will be set after validation, assuming there are no validation errors.
        if (!useDefaultResourceType && resource.getResourceType() == null) {
            errors.append("resourceType is a required property\n");
        }

        for (ResourceBuilder childBuilder : childBuilders) {
            String childErrors = childBuilder.validate();
            if (childErrors != null) {
                errors.append("The following child resource errors were found:\n" + childErrors);
            }
        }

        if (errors.length() == 0) {
            return null;
        }

        return "Unable to build Resource instance due to the following validation errors:\n" + errors;
    }

    private ResourceBuilder withDefaultResourceType() {
        switch (category) {
            case PLATFORM: return withDefaultPlatformResourceType();
            case SERVER:   return withDefaultServerResourceType();
            default:       return withDefaultServiceResourceType();
        }
    }

    /**
     * The default platform resource type is as its name implies a platform type whose name defaults to the resource
     * name and the plugin name (i.e., ResourceType.plugin property) defaults to resource name + 'Plugin'.
     *
     * @return The builder
     */
    private ResourceBuilder withDefaultPlatformResourceType() {
        resource.setResourceType(new ResourceTypeBuilder().createPlatformResourceType()
            .withName(resource.getName())
            .withPlugin(resource.getName() + " Plugin")
            .withParentResourceType(resource.getResourceType())
            .build());
        return this;
    }

    /**
     * The default server resource type is as its name implies a server type whose name defaults to the resource name
     * and the plugin name (i.e., ResourceType.plugin property) defaults to resource name + 'Plugin'
     *
     * @return The builder
     */
    private ResourceBuilder withDefaultServerResourceType() {
        resource.setResourceType(new ResourceTypeBuilder().createServerResourceType()
            .withName(resource.getName())
            .withPlugin(resource.getName() + " Plugin")
            .withParentResourceType(resource.getResourceType())
            .build());
        return this;
    }

    /**
     * The default service resource type is as its name implies a service type whose name defaults to the resource name
     * and the plugin name (i.e., ResourceType.plugin property) defaults to resource name + 'Plugin'
     *
     * @return The builder
     */
    private ResourceBuilder withDefaultServiceResourceType() {
        resource.setResourceType(new ResourceTypeBuilder().createServerResourceType()
            .withName(resource.getName())
            .withPlugin(resource.getName() + " Plugin")
            .withParentResourceType(resource.getResourceType())
            .build());
        return this;
    }

    private String randomString() {
        return new BigInteger(16, random).toString(32);
    }

}
