@Test
void walkDefaults() throws JsonProcessingException {
    String schemaData = "{\n" + "  \"prefixItems\": [\n" + "    {\n" + "      \"type\": \"integer\",\n" + "      \"default\": 1\n" + "    },\n" + "    {\n" + "      \"type\": \"string\",\n" + "      \"default\": \"2\"\n" + "    },\n" + "    {\n" + "      \"type\": \"integer\",\n" + "      \"default\": 3\n" + "    }\n" + "  ]\n" + "}";
    ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
            @SuppressWarnings("unchecked")
            List<WalkEvent> items = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext().getData().computeIfAbsent("items", key -> new ArrayList<NodePath>());
            items.add(walkEvent);
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)).itemWalkListenerRunner(itemWalkListenerRunner).build();
    SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12);
    Schema schema = factory.getSchema(schemaData);
    JsonNode input = JsonMapperFactory.getInstance().readTree("[null, null]");
    Result result = schema.walk(input, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertTrue(result.getErrors().isEmpty());
    @SuppressWarnings("unchecked")
    List<WalkEvent> items = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("items");
    assertEquals(3, items.size());
    assertEquals("/0", items.get(0).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(0).getKeyword());
    assertEquals("#/prefixItems/0", items.get(0).getSchema().getSchemaLocation().toString());
    assertEquals("/1", items.get(1).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(1).getKeyword());
    assertEquals("#/prefixItems/1", items.get(1).getSchema().getSchemaLocation().toString());
    assertEquals("/2", items.get(2).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(2).getKeyword());
    assertEquals("#/prefixItems/2", items.get(2).getSchema().getSchemaLocation().toString());
}

@Test
void walkDefaults() throws JsonProcessingException {
    String schemaData = "{\n" + "  \"prefixItems\": [\n" + "    {\n" + "      \"type\": \"integer\",\n" + "      \"default\": 1\n" + "    },\n" + "    {\n" + "      \"type\": \"string\",\n" + "      \"default\": \"2\"\n" + "    },\n" + "    {\n" + "      \"type\": \"integer\",\n" + "      \"default\": 3\n" + "    }\n" + "  ]\n" + "}";
    ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
            @SuppressWarnings("unchecked")
            List<WalkEvent> items = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext().getData().computeIfAbsent("items", key -> new ArrayList<NodePath>());
            items.add(walkEvent);
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)).itemWalkHandler(itemWalkHandler).build();
    SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12);
    Schema schema = factory.getSchema(schemaData);
    JsonNode input = JsonMapperFactory.getInstance().readTree("[null, null]");
    Result result = schema.walk(input, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertTrue(result.getErrors().isEmpty());
    @SuppressWarnings("unchecked")
    List<WalkEvent> items = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("items");
    assertEquals(3, items.size());
    assertEquals("/0", items.get(0).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(0).getKeyword());
    assertEquals("#/prefixItems/0", items.get(0).getSchema().getSchemaLocation().toString());
    assertEquals("/1", items.get(1).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(1).getKeyword());
    assertEquals("#/prefixItems/1", items.get(1).getSchema().getSchemaLocation().toString());
    assertEquals("/2", items.get(2).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(2).getKeyword());
    assertEquals("#/prefixItems/2", items.get(2).getSchema().getSchemaLocation().toString());
}

@Test
void walkNull() {
    String schemaData = "{\n" + "  \"prefixItems\": [\n" + "    {\n" + "      \"type\": \"integer\"\n" + "    },\n" + "    {\n" + "      \"type\": \"string\"\n" + "    },\n" + "    {\n" + "      \"type\": \"integer\"\n" + "    }\n" + "  ]\n" + "}";
    ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
            @SuppressWarnings("unchecked")
            List<WalkEvent> items = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext().getData().computeIfAbsent("items", key -> new ArrayList<NodePath>());
            items.add(walkEvent);
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner).build();
    SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12);
    Schema schema = factory.getSchema(schemaData);
    Result result = schema.walk(null, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertTrue(result.getErrors().isEmpty());
    @SuppressWarnings("unchecked")
    List<WalkEvent> items = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("items");
    assertEquals(3, items.size());
    assertEquals("/0", items.get(0).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(0).getKeyword());
    assertEquals("#/prefixItems/0", items.get(0).getSchema().getSchemaLocation().toString());
    assertEquals("/1", items.get(1).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(1).getKeyword());
    assertEquals("#/prefixItems/1", items.get(1).getSchema().getSchemaLocation().toString());
    assertEquals("/2", items.get(2).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(2).getKeyword());
    assertEquals("#/prefixItems/2", items.get(2).getSchema().getSchemaLocation().toString());
}

@Test
void walkNull() {
    String schemaData = "{\n" + "  \"prefixItems\": [\n" + "    {\n" + "      \"type\": \"integer\"\n" + "    },\n" + "    {\n" + "      \"type\": \"string\"\n" + "    },\n" + "    {\n" + "      \"type\": \"integer\"\n" + "    }\n" + "  ]\n" + "}";
    ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
            @SuppressWarnings("unchecked")
            List<WalkEvent> items = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext().getData().computeIfAbsent("items", key -> new ArrayList<NodePath>());
            items.add(walkEvent);
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler).build();
    SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12);
    Schema schema = factory.getSchema(schemaData);
    Result result = schema.walk(null, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertTrue(result.getErrors().isEmpty());
    @SuppressWarnings("unchecked")
    List<WalkEvent> items = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("items");
    assertEquals(3, items.size());
    assertEquals("/0", items.get(0).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(0).getKeyword());
    assertEquals("#/prefixItems/0", items.get(0).getSchema().getSchemaLocation().toString());
    assertEquals("/1", items.get(1).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(1).getKeyword());
    assertEquals("#/prefixItems/1", items.get(1).getSchema().getSchemaLocation().toString());
    assertEquals("/2", items.get(2).getInstanceLocation().toString());
    assertEquals("prefixItems", items.get(2).getKeyword());
    assertEquals("#/prefixItems/2", items.get(2).getSchema().getSchemaLocation().toString());
}


@Test
void shouldCallAllKeywordListenerOnWalkStart() throws Exception {
    AllKeywordListener allKeywordListener = new AllKeywordListener();
    KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder().keywordWalkListener(allKeywordListener).build();
    WalkConfig walkConfig = WalkConfig.builder().keywordWalkListenerRunner(keywordWalkListenerRunner).build();
    SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
    SchemaLocation draft07Schema = SchemaLocation.of("resource:/draft-07/schema#");
    Schema firstSchema = factory.getSchema(draft07Schema);
    firstSchema.walk(new ObjectMapper().readTree("{ \"id\": 123 }"), true);
    Schema secondSchema = factory.getSchema(draft07Schema);
    secondSchema.walk(new ObjectMapper().readTree("{ \"id\": 123 }"), true, executionContext -> executionContext.setWalkConfig(walkConfig));
    Assertions.assertTrue(allKeywordListener.wasCalled);
}

@Test
void shouldCallAllKeywordListenerOnWalkStart() throws Exception {
    AllKeywordListener allKeywordListener = new AllKeywordListener();
    KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder().keywordWalkListener(allKeywordListener).build();
    WalkConfig walkConfig = WalkConfig.builder().keywordWalkHandler(keywordWalkHandler).build();
    SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
    SchemaLocation draft07Schema = SchemaLocation.of("resource:/draft-07/schema#");
    Schema firstSchema = factory.getSchema(draft07Schema);
    firstSchema.walk(new ObjectMapper().readTree("{ \"id\": 123 }"), true);
    Schema secondSchema = factory.getSchema(draft07Schema);
    secondSchema.walk(new ObjectMapper().readTree("{ \"id\": 123 }"), true, executionContext -> executionContext.setWalkConfig(walkConfig));
    Assertions.assertTrue(allKeywordListener.wasCalled);
}

public Builder keywordWalkListenerRunner(WalkListenerRunner keywordWalkListenerRunner) {
    this.keywordWalkListenerRunner = keywordWalkListenerRunner;
    return this;
}


@Test
void evaluationPathWalk() {
    PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder().keywordWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012());
    String schemaData = "{\n" + "  \"type\": \"object\",\n" + "  \"properties\": {\n" + "    \"productId\": {\n" + "      \"type\": \"integer\",\n" + "      \"minimum\": 1\n" + "    }\n" + "  },\n" + "  \"additionalProperties\": {\n" + "    \"type\": \"integer\"\n" + "  }\n" + "}";
    String instanceData = "{\n" + "  \"productId\": 0,\n" + "  \"product\": \"hello\"\n" + "}";
    Schema schema = schemaRegistry.getSchema(schemaData, InputFormat.JSON);
    Result result = schema.walk(instanceData, InputFormat.JSON, true, executionContext -> executionContext.walkConfig(walkConfig -> walkConfig.propertyWalkListenerRunner(propertyWalkListenerRunner).keywordWalkListenerRunner(keywordWalkListenerRunner)));
    List<Error> errors = result.getErrors();
    assertEquals(2, errors.size());
    assertEquals("/properties/productId/minimum", errors.get(0).getEvaluationPath().toString());
    assertEquals("#/properties/productId/minimum", errors.get(0).getSchemaLocation().toString());
    assertEquals("minimum", errors.get(0).getKeyword());
    assertEquals("/additionalProperties/type", errors.get(1).getEvaluationPath().toString());
    assertEquals("#/additionalProperties/type", errors.get(1).getSchemaLocation().toString());
    assertEquals("type", errors.get(1).getKeyword());
}

@Test
void evaluationPathWalk() {
    PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder().keywordWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012());
    String schemaData = "{\n" + "  \"type\": \"object\",\n" + "  \"properties\": {\n" + "    \"productId\": {\n" + "      \"type\": \"integer\",\n" + "      \"minimum\": 1\n" + "    }\n" + "  },\n" + "  \"additionalProperties\": {\n" + "    \"type\": \"integer\"\n" + "  }\n" + "}";
    String instanceData = "{\n" + "  \"productId\": 0,\n" + "  \"product\": \"hello\"\n" + "}";
    Schema schema = schemaRegistry.getSchema(schemaData, InputFormat.JSON);
    Result result = schema.walk(instanceData, InputFormat.JSON, true, executionContext -> executionContext.walkConfig(walkConfig -> walkConfig.propertyWalkHandler(propertyWalkHandler).keywordWalkHandler(keywordWalkHandler)));
    List<Error> errors = result.getErrors();
    assertEquals(2, errors.size());
    assertEquals("/properties/productId/minimum", errors.get(0).getEvaluationPath().toString());
    assertEquals("#/properties/productId/minimum", errors.get(0).getSchemaLocation().toString());
    assertEquals("minimum", errors.get(0).getKeyword());
    assertEquals("/additionalProperties/type", errors.get(1).getEvaluationPath().toString());
    assertEquals("#/additionalProperties/type", errors.get(1).getSchemaLocation().toString());
    assertEquals("type", errors.get(1).getKeyword());
}

public PropertyWalkListenerRunner build() {
    return new PropertyWalkListenerRunner(propertyWalkListeners);
}


@Test
void applyDefaultsWithWalker() throws JsonProcessingException {
    String schemaData = "{\r\n" + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + "  \"title\": \"\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"s\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": \"S\"\r\n" + "    },\r\n" + "    \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n" + "  },\r\n" + "  \"required\": [ \"s\", \"ref\" ],\r\n" + "\r\n" + "  \"$defs\": {\r\n" + "    \"r\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": \"REF\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode() || walkEvent.getInstanceNode().isNull()) {
                Schema schema = walkEvent.getSchema();
                SchemaRef schemaRef = SchemaRefs.from(schema, walkEvent.getExecutionContext());
                if (schemaRef != null) {
                    schema = schemaRef.getSchema();
                }
                JsonNode defaultNode = schema.getSchemaNode().get("default");
                if (defaultNode != null) {
                    ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent());
                    parentNode.set(walkEvent.getInstanceLocation().getName(-1), defaultNode);
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkListenerRunner(propertyWalkListenerRunner).build();
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"s\":\"S\",\"ref\":\"REF\"}", inputNode.toString());
    assertTrue(result.getErrors().isEmpty());
}

@Test
void applyDefaultsWithWalker() throws JsonProcessingException {
    String schemaData = "{\r\n" + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + "  \"title\": \"\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"s\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": \"S\"\r\n" + "    },\r\n" + "    \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n" + "  },\r\n" + "  \"required\": [ \"s\", \"ref\" ],\r\n" + "\r\n" + "  \"$defs\": {\r\n" + "    \"r\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": \"REF\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode() || walkEvent.getInstanceNode().isNull()) {
                Schema schema = walkEvent.getSchema();
                SchemaRef schemaRef = SchemaRefs.from(schema, walkEvent.getExecutionContext());
                if (schemaRef != null) {
                    schema = schemaRef.getSchema();
                }
                JsonNode defaultNode = schema.getSchemaNode().get("default");
                if (defaultNode != null) {
                    ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent());
                    parentNode.set(walkEvent.getInstanceLocation().getName(-1), defaultNode);
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkHandler(propertyWalkHandler).build();
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"s\":\"S\",\"ref\":\"REF\"}", inputNode.toString());
    assertTrue(result.getErrors().isEmpty());
}

public Builder propertyWalkHandler(WalkHandler propertyWalkHandler) {
    this.propertyWalkHandler = propertyWalkHandler;
    return this;
}

@Test
void generateDataWithWalker() throws JsonProcessingException {
    Map<String, Supplier<String>> generators = new HashMap<>();
    generators.put("name.findName", () -> "John Doe");
    generators.put("internet.email", () -> "john.doe@gmail.com");
    String schemaData = "{\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"name\": {\r\n" + "      \"$ref\": \"#/$defs/Name\"\r\n" + "    },\r\n" + "    \"email\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"faker\": \"internet.email\"\r\n" + "    }\r\n" + "  },\r\n" + "  \"required\": [\r\n" + "    \"name\",\r\n" + "    \"email\"\r\n" + "  ],\r\n" + "  \"$defs\": {\r\n" + "    \"Name\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"faker\": \"name.findName\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode() || walkEvent.getInstanceNode().isNull()) {
                Schema schema = walkEvent.getSchema();
                SchemaRef schemaRef = null;
                do {
                    schemaRef = SchemaRefs.from(schema, walkEvent.getExecutionContext());
                    if (schemaRef != null) {
                        schema = schemaRef.getSchema();
                    }
                } while (schemaRef != null);
                JsonNode fakerNode = schema.getSchemaNode().get("faker");
                if (fakerNode != null) {
                    String faker = fakerNode.asText();
                    String fakeData = generators.get(faker).get();
                    JsonNode fakeDataNode = JsonNodeFactory.instance.textNode(fakeData);
                    ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent());
                    parentNode.set(walkEvent.getInstanceLocation().getName(-1), fakeDataNode);
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkListenerRunner(propertyWalkListenerRunner).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"name\":\"John Doe\",\"email\":\"john.doe@gmail.com\"}", inputNode.toString());
    assertTrue(result.getErrors().isEmpty());
}

@Test
void generateDataWithWalker() throws JsonProcessingException {
    Map<String, Supplier<String>> generators = new HashMap<>();
    generators.put("name.findName", () -> "John Doe");
    generators.put("internet.email", () -> "john.doe@gmail.com");
    String schemaData = "{\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"name\": {\r\n" + "      \"$ref\": \"#/$defs/Name\"\r\n" + "    },\r\n" + "    \"email\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"faker\": \"internet.email\"\r\n" + "    }\r\n" + "  },\r\n" + "  \"required\": [\r\n" + "    \"name\",\r\n" + "    \"email\"\r\n" + "  ],\r\n" + "  \"$defs\": {\r\n" + "    \"Name\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"faker\": \"name.findName\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode() || walkEvent.getInstanceNode().isNull()) {
                Schema schema = walkEvent.getSchema();
                SchemaRef schemaRef = null;
                do {
                    schemaRef = SchemaRefs.from(schema, walkEvent.getExecutionContext());
                    if (schemaRef != null) {
                        schema = schemaRef.getSchema();
                    }
                } while (schemaRef != null);
                JsonNode fakerNode = schema.getSchemaNode().get("faker");
                if (fakerNode != null) {
                    String faker = fakerNode.asText();
                    String fakeData = generators.get(faker).get();
                    JsonNode fakeDataNode = JsonNodeFactory.instance.textNode(fakeData);
                    ObjectNode parentNode = JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent());
                    parentNode.set(walkEvent.getInstanceLocation().getName(-1), fakeDataNode);
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkHandler(propertyWalkHandler).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"name\":\"John Doe\",\"email\":\"john.doe@gmail.com\"}", inputNode.toString());
    assertTrue(result.getErrors().isEmpty());
}

@Test
void applyInvalidDefaultsWithWalker() throws JsonProcessingException {
    String schemaData = "{\r\n" + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + "  \"title\": \"\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"s\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": 1\r\n" + "    },\r\n" + "    \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n" + "  },\r\n" + "  \"required\": [ \"s\", \"ref\" ],\r\n" + "\r\n" + "  \"$defs\": {\r\n" + "    \"r\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": \"REF\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode() || walkEvent.getInstanceNode().isNull()) {
                Schema schema = walkEvent.getSchema();
                SchemaRef schemaRef = SchemaRefs.from(schema, walkEvent.getExecutionContext());
                if (schemaRef != null) {
                    schema = schemaRef.getSchema();
                }
                JsonNode defaultNode = schema.getSchemaNode().get("default");
                if (defaultNode != null) {
                    ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent());
                    parentNode.set(walkEvent.getInstanceLocation().getName(-1), defaultNode);
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkListenerRunner(propertyWalkListenerRunner).build();
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString());
    assertFalse(result.getErrors().isEmpty());
    inputNode = JsonMapperFactory.getInstance().readTree("{}");
    result = schema.walk(inputNode, false, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString());
    assertTrue(result.getErrors().isEmpty());
}

@Test
void applyInvalidDefaultsWithWalker() throws JsonProcessingException {
    String schemaData = "{\r\n" + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + "  \"title\": \"\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"s\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": 1\r\n" + "    },\r\n" + "    \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n" + "  },\r\n" + "  \"required\": [ \"s\", \"ref\" ],\r\n" + "\r\n" + "  \"$defs\": {\r\n" + "    \"r\": {\r\n" + "      \"type\": \"string\",\r\n" + "      \"default\": \"REF\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode() || walkEvent.getInstanceNode().isNull()) {
                Schema schema = walkEvent.getSchema();
                SchemaRef schemaRef = SchemaRefs.from(schema, walkEvent.getExecutionContext());
                if (schemaRef != null) {
                    schema = schemaRef.getSchema();
                }
                JsonNode defaultNode = schema.getSchemaNode().get("default");
                if (defaultNode != null) {
                    ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent());
                    parentNode.set(walkEvent.getInstanceLocation().getName(-1), defaultNode);
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkHandler(propertyWalkHandler).build();
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString());
    assertFalse(result.getErrors().isEmpty());
    inputNode = JsonMapperFactory.getInstance().readTree("{}");
    result = schema.walk(inputNode, false, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString());
    assertTrue(result.getErrors().isEmpty());
}

@Test
void missingRequired() throws JsonProcessingException {
    String schemaData = "{\r\n" + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + "  \"title\": \"\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"s\": {\r\n" + "      \"type\": \"integer\"\r\n" + "    },\r\n" + "    \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n" + "  },\r\n" + "  \"required\": [ \"s\", \"ref\" ],\r\n" + "\r\n" + "  \"$defs\": {\r\n" + "    \"r\": {\r\n" + "      \"type\": \"string\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    Map<String, JsonNode> missingSchemaNode = new LinkedHashMap<>();
    KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder().keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            JsonNode requiredNode = walkEvent.getSchema().getSchemaNode().get("required");
            List<String> requiredProperties = new ArrayList<>();
            if (requiredNode != null) {
                if (requiredNode.isArray()) {
                    for (JsonNode fieldName : requiredNode) {
                        requiredProperties.add(fieldName.asText());
                    }
                }
            }
            for (String requiredProperty : requiredProperties) {
                JsonNode propertyNode = walkEvent.getInstanceNode().get(requiredProperty);
                if (propertyNode == null) {
                    PropertiesValidator propertiesValidator = walkEvent.getValidator();
                    Schema propertySchema = propertiesValidator.getSchemas().get(requiredProperty);
                    SchemaRef schemaRef = SchemaRefs.from(propertySchema, walkEvent.getExecutionContext());
                    if (schemaRef != null) {
                        propertySchema = schemaRef.getSchema();
                    }
                    missingSchemaNode.put(requiredProperty, propertySchema.getSchemaNode());
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().keywordWalkListenerRunner(keywordWalkListenerRunner).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertFalse(result.getErrors().isEmpty());
    assertEquals("{\"type\":\"integer\"}", missingSchemaNode.get("s").toString());
    assertEquals("{\"type\":\"string\"}", missingSchemaNode.get("ref").toString());
}

@Test
void missingRequired() throws JsonProcessingException {
    String schemaData = "{\r\n" + "  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + "  \"title\": \"\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"properties\": {\r\n" + "    \"s\": {\r\n" + "      \"type\": \"integer\"\r\n" + "    },\r\n" + "    \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n" + "  },\r\n" + "  \"required\": [ \"s\", \"ref\" ],\r\n" + "\r\n" + "  \"$defs\": {\r\n" + "    \"r\": {\r\n" + "      \"type\": \"string\"\r\n" + "    }\r\n" + "  }\r\n" + "}";
    Map<String, JsonNode> missingSchemaNode = new LinkedHashMap<>();
    KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder().keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            JsonNode requiredNode = walkEvent.getSchema().getSchemaNode().get("required");
            List<String> requiredProperties = new ArrayList<>();
            if (requiredNode != null) {
                if (requiredNode.isArray()) {
                    for (JsonNode fieldName : requiredNode) {
                        requiredProperties.add(fieldName.asText());
                    }
                }
            }
            for (String requiredProperty : requiredProperties) {
                JsonNode propertyNode = walkEvent.getInstanceNode().get(requiredProperty);
                if (propertyNode == null) {
                    PropertiesValidator propertiesValidator = walkEvent.getValidator();
                    Schema propertySchema = propertiesValidator.getSchemas().get(requiredProperty);
                    SchemaRef schemaRef = SchemaRefs.from(propertySchema, walkEvent.getExecutionContext());
                    if (schemaRef != null) {
                        propertySchema = schemaRef.getSchema();
                    }
                    missingSchemaNode.put(requiredProperty, propertySchema.getSchemaNode());
                }
            }
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    WalkConfig walkConfig = WalkConfig.builder().keywordWalkHandler(keywordWalkHandler).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);
    JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
    Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertFalse(result.getErrors().isEmpty());
    assertEquals("{\"type\":\"integer\"}", missingSchemaNode.get("s").toString());
    assertEquals("{\"type\":\"string\"}", missingSchemaNode.get("ref").toString());
}

public KeywordWalkHandler build() {
    return new KeywordWalkHandler(allKeywordWalkListeners, keywordWalkListeners);
}

@Test
void propertyListener() {
    String schemaData = "{\r\n" + "  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"description\": \"Default Description\",\r\n" + "  \"properties\": {\r\n" + "    \"tags\": {\r\n" + "      \"type\": \"array\",\r\n" + "      \"items\": {\r\n" + "        \"$ref\": \"#/definitions/tag\"\r\n" + "      }\r\n" + "    }\r\n" + "  },\r\n" + "  \"definitions\": {\r\n" + "    \"tag\": {\r\n" + "      \"properties\": {\r\n" + "        \"name\": {\r\n" + "          \"type\": \"string\"\r\n" + "        },\r\n" + "        \"description\": {\r\n" + "          \"type\": \"string\"\r\n" + "        }\r\n" + "      }\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            @SuppressWarnings("unchecked")
            List<WalkEvent> properties = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext().getData().computeIfAbsent("properties", key -> new ArrayList<>());
            properties.add(walkEvent);
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7).getSchema(schemaData);
    String inputData = "{\r\n" + "  \"tags\": [\r\n" + "    {\r\n" + "      \"name\": \"image\",\r\n" + "      \"description\": \"An image\"\r\n" + "    },\r\n" + "    {\r\n" + "      \"name\": \"link\",\r\n" + "      \"description\": \"A link\"\r\n" + "    }\r\n" + "  ]\r\n" + "}";
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkListenerRunner(propertyWalkListenerRunner).build();
    Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertTrue(result.getErrors().isEmpty());
    @SuppressWarnings("unchecked")
    List<WalkEvent> properties = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("properties");
    assertEquals(5, properties.size());
    assertEquals("properties", properties.get(0).getValidator().getKeyword());
    assertEquals("/tags", properties.get(0).getInstanceLocation().toString());
    assertEquals("/properties/tags", properties.get(0).getEvaluationPath().toString());
    assertEquals("/tags/0/name", properties.get(1).getInstanceLocation().toString());
    assertEquals("image", properties.get(1).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/name", properties.get(1).getEvaluationPath().toString());
    assertEquals("/tags/0/description", properties.get(2).getInstanceLocation().toString());
    assertEquals("An image", properties.get(2).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/description", properties.get(2).getEvaluationPath().toString());
    assertEquals("/tags/1/name", properties.get(3).getInstanceLocation().toString());
    assertEquals("link", properties.get(3).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/name", properties.get(3).getEvaluationPath().toString());
    assertEquals("/tags/1/description", properties.get(4).getInstanceLocation().toString());
    assertEquals("A link", properties.get(4).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/description", properties.get(4).getEvaluationPath().toString());
}

@Test
void propertyListener() {
    String schemaData = "{\r\n" + "  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + "  \"type\": \"object\",\r\n" + "  \"description\": \"Default Description\",\r\n" + "  \"properties\": {\r\n" + "    \"tags\": {\r\n" + "      \"type\": \"array\",\r\n" + "      \"items\": {\r\n" + "        \"$ref\": \"#/definitions/tag\"\r\n" + "      }\r\n" + "    }\r\n" + "  },\r\n" + "  \"definitions\": {\r\n" + "    \"tag\": {\r\n" + "      \"properties\": {\r\n" + "        \"name\": {\r\n" + "          \"type\": \"string\"\r\n" + "        },\r\n" + "        \"description\": {\r\n" + "          \"type\": \"string\"\r\n" + "        }\r\n" + "      }\r\n" + "    }\r\n" + "  }\r\n" + "}";
    PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(new WalkListener() {

        @Override
        public WalkFlow onWalkStart(WalkEvent walkEvent) {
            List<WalkEvent> properties = walkEvent.getExecutionContext().getCollectorContext().computeIfAbsent("properties", key -> new ArrayList<>());
            properties.add(walkEvent);
            return WalkFlow.CONTINUE;
        }

        @Override
        public void onWalkEnd(WalkEvent walkEvent, List<Error> errors) {
        }
    }).build();
    Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7).getSchema(schemaData);
    String inputData = "{\r\n" + "  \"tags\": [\r\n" + "    {\r\n" + "      \"name\": \"image\",\r\n" + "      \"description\": \"An image\"\r\n" + "    },\r\n" + "    {\r\n" + "      \"name\": \"link\",\r\n" + "      \"description\": \"A link\"\r\n" + "    }\r\n" + "  ]\r\n" + "}";
    WalkConfig walkConfig = WalkConfig.builder().propertyWalkHandler(propertyWalkHandler).build();
    Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig));
    assertTrue(result.getErrors().isEmpty());
    @SuppressWarnings("unchecked")
    List<WalkEvent> properties = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("properties");
    assertEquals(5, properties.size());
    assertEquals("properties", properties.get(0).getValidator().getKeyword());
    assertEquals("/tags", properties.get(0).getInstanceLocation().toString());
    assertEquals("/properties/tags", properties.get(0).getEvaluationPath().toString());
    assertEquals("/tags/0/name", properties.get(1).getInstanceLocation().toString());
    assertEquals("image", properties.get(1).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/name", properties.get(1).getEvaluationPath().toString());
    assertEquals("/tags/0/description", properties.get(2).getInstanceLocation().toString());
    assertEquals("An image", properties.get(2).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/description", properties.get(2).getEvaluationPath().toString());
    assertEquals("/tags/1/name", properties.get(3).getInstanceLocation().toString());
    assertEquals("link", properties.get(3).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/name", properties.get(3).getEvaluationPath().toString());
    assertEquals("/tags/1/description", properties.get(4).getInstanceLocation().toString());
    assertEquals("A link", properties.get(4).getInstanceNode().asText());
    assertEquals("/properties/tags/items/$ref/properties/description", properties.get(4).getEvaluationPath().toString());
}

public Builder propertyWalkListener(WalkListener propertyWalkListener) {
    this.propertyWalkListeners.add(propertyWalkListener);
    return this;
}


@Test
public void testHotspot() throws Exception {
    Random seeder = new Random();
    for (int c = 0; c < ITERATIONS; c++) {
        int seed = seeder.nextInt();
        Class<?> cl = ClassGenerator.generate(new Random(seed), 5, 50);
        ClassData cd = ClassData.parseClass(cl);
        try {
            for (DataModel model : MODELS) {
                for (boolean hierarchyGaps : BOOLS) {
                    for (boolean superClassGaps : BOOLS) {
                        for (boolean autoAlign : BOOLS) {
                            for (boolean compactFields : BOOLS) {
                                for (int fieldAllocationStyle : new int[] { 0, 1, 2 }) {
                                    HotSpotLayouter layouter = new HotSpotLayouter(model, hierarchyGaps, superClassGaps, autoAlign, compactFields, fieldAllocationStyle);
                                    layouter.layout(cd);
                                }
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            Assert.fail("Failed. Seed = " + seed);
        }
    }
}

@Test
public void testHotspot() throws Exception {
    Random seeder = new Random();
    for (int c = 0; c < ITERATIONS; c++) {
        int seed = seeder.nextInt();
        Class<?> cl = ClassGenerator.generate(new Random(seed), 5, 50);
        ClassData cd = ClassData.parseClass(cl);
        try {
            for (DataModel model : MODELS) {
                HotSpotLayouter layouter = new HotSpotLayouter(model);
                layouter.layout(cd);
            }
        } catch (Exception e) {
            Assert.fail("Failed. Seed = " + seed);
        }
    }
}

@Override
public ClassLayout layout(ClassData cd) {
    SortedSet<FieldLayout> result = new TreeSet<>();
    if (cd.isArray()) {
        int base = model.arrayHeaderSize();
        int scale = model.sizeOf(cd.arrayComponentType());
        long instanceSize = base + cd.arrayLength() * scale;
        instanceSize = MathUtil.align(instanceSize, model.objectAlignment());
        base = MathUtil.align(base, Math.max(4, scale));
        result.add(new FieldLayout(FieldData.create(cd.arrayClass(), "<elements>", cd.arrayComponentType()), base, scale * cd.arrayLength()));
        return new ClassLayout(cd, result, model.arrayHeaderSize(), instanceSize, false);
    }
    List<ClassData> classDataClassHierarchy = new ArrayList<>();
    ClassData cld = cd;
    classDataClassHierarchy.add(cld);
    while ((cld = cld.superClass()) != null) {
        classDataClassHierarchy.add(0, cld);
    }
    int superClassLastOopOffset = 0;
    int superClassFieldsSize = 0;
    int nextPaddedOffset = 0;
    List<Integer> superGapsOffsets = new ArrayList<>();
    List<Integer> superGapsSizes = new ArrayList<>();
    for (ClassData clsData : classDataClassHierarchy) {
        EnumMap<FieldAllocationType, Integer> fieldsAllocationCount = new EnumMap<>(FieldAllocationType.class);
        EnumMap<FieldAllocationType, Integer> nextOffset = new EnumMap<>(FieldAllocationType.class);
        EnumMap<FieldAllocationType, ArrayDeque<Integer>> spaceOffset = new EnumMap<>(FieldAllocationType.class);
        EnumMap<FieldAllocationType, Integer> allocationTypeSizes = new EnumMap<>(FieldAllocationType.class);
        for (FieldAllocationType atype : FieldAllocationType.values()) {
            fieldsAllocationCount.put(atype, 0);
            nextOffset.put(atype, 0);
            spaceOffset.put(atype, new ArrayDeque<Integer>());
        }
        allocationTypeSizes.put(OOP, model.sizeOf("oop"));
        allocationTypeSizes.put(BYTE, model.sizeOf("byte"));
        allocationTypeSizes.put(SHORT, model.sizeOf("short"));
        allocationTypeSizes.put(WORD, model.sizeOf("int"));
        allocationTypeSizes.put(DOUBLE, model.sizeOf("long"));
        for (FieldData f : clsData.ownFields()) {
            FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
            Integer count = fieldsAllocationCount.get(atype);
            fieldsAllocationCount.put(atype, ++count);
        }
        int contendedCount = 0;
        EnumMap<FieldAllocationType, Integer> facContended = new EnumMap<>(FieldAllocationType.class);
        for (FieldData f : clsData.ownFields()) {
            FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
            if (f.isContended()) {
                Integer count = facContended.get(atype);
                facContended.put(atype, count == null ? 1 : ++count);
                contendedCount++;
            }
        }
        int fieldsStart = (clsData.superClass() == null ? model.headerSize() : 0) + superClassFieldsSize;
        int nextFieldOffset = fieldsStart;
        boolean isContendedClass = clsData.isContended();
        if (isContendedClass) {
            nextFieldOffset += CONTENDED_PADDING_WIDTH;
        }
        int doubleCount = fieldsAllocationCount.get(DOUBLE) - (facContended.containsKey(DOUBLE) ? facContended.get(DOUBLE) : 0);
        int wordCount = fieldsAllocationCount.get(WORD) - (facContended.containsKey(WORD) ? facContended.get(WORD) : 0);
        int shortCount = fieldsAllocationCount.get(SHORT) - (facContended.containsKey(SHORT) ? facContended.get(SHORT) : 0);
        int byteCount = fieldsAllocationCount.get(BYTE) - (facContended.containsKey(BYTE) ? facContended.get(BYTE) : 0);
        int oopCount = fieldsAllocationCount.get(OOP) - (facContended.containsKey(OOP) ? facContended.get(OOP) : 0);
        int firstOopOffset = 0;
        boolean compactFields = this.compactFields;
        int allocationStyle = this.fieldAllocationStyle;
        if (allocationStyle < 0 || allocationStyle > 2) {
            allocationStyle = 1;
        }
        if ((allocationStyle != 0 || compactFields) && PREDEF_OFFSETS.contains(clsData.name())) {
            allocationStyle = 0;
            compactFields = false;
        }
        if (allocationStyle == 0) {
            nextOffset.put(OOP, nextFieldOffset);
            nextOffset.put(DOUBLE, nextOffset.get(OOP) + (oopCount * model.sizeOf("oop")));
        } else if (allocationStyle == 1) {
            nextOffset.put(DOUBLE, nextFieldOffset);
        } else if (allocationStyle == 2) {
            if (superClassFieldsSize > 0) {
                if (clsData.superClass() != null && clsData.superClass().oopsCount() > 0) {
                    if (superClassLastOopOffset + model.sizeOf("oop") == nextFieldOffset) {
                        allocationStyle = 0;
                        nextOffset.put(OOP, nextFieldOffset);
                        nextOffset.put(DOUBLE, nextOffset.get(OOP) + (oopCount * model.sizeOf("oop")));
                    }
                }
            }
            if (allocationStyle == 2) {
                allocationStyle = 1;
                nextOffset.put(DOUBLE, nextFieldOffset);
            }
        } else {
            throw new IllegalStateException();
        }
        if (takeHierarchyGaps || takeSuperGaps) {
            ListIterator<Integer> itSuperGapsOffsets = superGapsOffsets.listIterator();
            ListIterator<Integer> itSuperGapsSizes = superGapsSizes.listIterator();
            int currentGapIndex = 0;
            while (itSuperGapsOffsets.hasNext() && itSuperGapsSizes.hasNext()) {
                int offset = itSuperGapsOffsets.next();
                int length = itSuperGapsSizes.next();
                if (length >= allocationTypeSizes.get(WORD) && wordCount > 0) {
                    int alignedWordOffset = MathUtil.align(offset, allocationTypeSizes.get(WORD));
                    if (alignedWordOffset + allocationTypeSizes.get(WORD) <= offset + length) {
                        wordCount -= 1;
                        spaceOffset.get(WORD).push(alignedWordOffset);
                        length -= allocationTypeSizes.get(WORD);
                        if (length >= allocationTypeSizes.get(SHORT) && shortCount > 0) {
                            int alignedShortOffset = MathUtil.align(offset, allocationTypeSizes.get(SHORT));
                            if (alignedShortOffset + allocationTypeSizes.get(SHORT) <= alignedWordOffset) {
                                shortCount -= 1;
                                spaceOffset.get(SHORT).push(alignedShortOffset);
                                length -= allocationTypeSizes.get(SHORT);
                                if (length >= allocationTypeSizes.get(BYTE) && byteCount > 0) {
                                    byteCount -= 1;
                                    spaceOffset.get(BYTE).push(offset);
                                    length -= allocationTypeSizes.get(BYTE);
                                }
                            } else {
                                throw new IllegalStateException();
                            }
                        }
                    } else {
                        throw new IllegalStateException();
                    }
                } else {
                    while (length >= allocationTypeSizes.get(SHORT) && shortCount > 0) {
                        int alignedShortOffset = MathUtil.align(offset, allocationTypeSizes.get(SHORT));
                        int shortAlignmentGapSize = alignedShortOffset - offset;
                        int shortAlignmentGapOffset = offset;
                        if (alignedShortOffset + allocationTypeSizes.get(SHORT) <= offset + length) {
                            shortCount -= 1;
                            spaceOffset.get(SHORT).push(alignedShortOffset);
                            length -= allocationTypeSizes.get(SHORT);
                            offset = alignedShortOffset + allocationTypeSizes.get(SHORT);
                            if (shortAlignmentGapSize != 0) {
                                if (byteCount > 0) {
                                    byteCount -= 1;
                                    spaceOffset.get(BYTE).push(shortAlignmentGapOffset);
                                } else {
                                    itSuperGapsOffsets.previous();
                                    itSuperGapsSizes.previous();
                                    itSuperGapsOffsets.add(shortAlignmentGapOffset);
                                    itSuperGapsSizes.add(shortAlignmentGapSize);
                                    currentGapIndex++;
                                    itSuperGapsOffsets.next();
                                    itSuperGapsSizes.next();
                                }
                                length -= allocationTypeSizes.get(BYTE);
                            }
                        }
                    }
                }
                while (length > 0 && byteCount > 0) {
                    byteCount -= 1;
                    spaceOffset.get(BYTE).push(offset);
                    length -= allocationTypeSizes.get(BYTE);
                    offset += allocationTypeSizes.get(BYTE);
                }
                if (length == 0) {
                    itSuperGapsOffsets.remove();
                    itSuperGapsSizes.remove();
                    continue;
                } else {
                    superGapsOffsets.set(currentGapIndex, offset);
                    superGapsSizes.set(currentGapIndex, length);
                }
                currentGapIndex++;
            }
        }
        if (doubleCount > 0) {
            int offset = nextOffset.get(DOUBLE);
            nextOffset.put(DOUBLE, MathUtil.align(offset, allocationTypeSizes.get(DOUBLE)));
            if (offset != nextOffset.get(DOUBLE)) {
                int length = nextOffset.get(DOUBLE) - offset;
                if (compactFields) {
                    if (wordCount > 0) {
                        wordCount -= 1;
                        spaceOffset.get(WORD).push(offset);
                        length -= allocationTypeSizes.get(WORD);
                        offset += allocationTypeSizes.get(WORD);
                    }
                    while (length >= allocationTypeSizes.get(SHORT) && shortCount > 0) {
                        shortCount -= 1;
                        spaceOffset.get(SHORT).push(offset);
                        length -= allocationTypeSizes.get(SHORT);
                        offset += allocationTypeSizes.get(SHORT);
                    }
                    while (length > 0 && byteCount > 0) {
                        byteCount -= 1;
                        spaceOffset.get(BYTE).push(offset);
                        length -= allocationTypeSizes.get(BYTE);
                        offset += allocationTypeSizes.get(BYTE);
                    }
                    if (length >= allocationTypeSizes.get(OOP) && oopCount > 0 && allocationStyle != 0) {
                        oopCount -= 1;
                        spaceOffset.get(OOP).push(offset);
                        length -= allocationTypeSizes.get(OOP);
                        offset += allocationTypeSizes.get(OOP);
                    }
                }
                if (takeSuperGaps && length > 0) {
                    superGapsOffsets.add(offset);
                    superGapsSizes.add(length);
                }
            }
        }
        nextOffset.put(WORD, nextOffset.get(DOUBLE) + (doubleCount * allocationTypeSizes.get(DOUBLE)));
        nextOffset.put(SHORT, nextOffset.get(WORD) + (wordCount * allocationTypeSizes.get(WORD)));
        nextOffset.put(BYTE, nextOffset.get(SHORT) + (shortCount * allocationTypeSizes.get(SHORT)));
        nextPaddedOffset = nextOffset.get(BYTE) + byteCount;
        if (allocationStyle == 1) {
            nextOffset.put(OOP, nextPaddedOffset);
            if (oopCount > 0) {
                nextOffset.put(OOP, MathUtil.align(nextOffset.get(OOP), allocationTypeSizes.get(OOP)));
            }
            nextPaddedOffset = nextOffset.get(OOP) + (oopCount * allocationTypeSizes.get(OOP));
        }
        Set<FieldData> layoutedFields = new HashSet<>();
        for (FieldData f : clsData.ownFields()) {
            if (layoutedFields.contains(f))
                continue;
            if (f.isContended())
                continue;
            FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
            int allocationTypeSize = allocationTypeSizes.get(atype);
            Integer allocationTypeSpaceOffset = spaceOffset.get(atype).poll();
            int realOffset;
            if (atype == DOUBLE) {
                int nextDoubleOffset = nextOffset.get(DOUBLE);
                realOffset = nextOffset.get(DOUBLE);
                nextOffset.put(atype, nextDoubleOffset + allocationTypeSize);
            } else {
                if (allocationTypeSpaceOffset != null) {
                    realOffset = allocationTypeSpaceOffset;
                } else {
                    int allocationTypeNextOffset = nextOffset.get(atype);
                    realOffset = allocationTypeNextOffset;
                    nextOffset.put(atype, allocationTypeNextOffset + allocationTypeSize);
                }
            }
            layoutedFields.add(f);
            result.add(new FieldLayout(f, realOffset, model.sizeOf(f.typeClass())));
            if (atype == OOP) {
                superClassLastOopOffset = realOffset;
            }
        }
        if (contendedCount > 0) {
            nextPaddedOffset += CONTENDED_PADDING_WIDTH;
            HashSet<String> contendedGroups = new HashSet<>();
            for (FieldData f : clsData.ownFields()) {
                if (f.isContended()) {
                    contendedGroups.add(f.contendedGroup());
                }
            }
            for (String currentGroup : contendedGroups) {
                for (FieldData f : clsData.ownFields()) {
                    if (layoutedFields.contains(f))
                        continue;
                    if (!f.isContended() || !f.contendedGroup().equals(currentGroup))
                        continue;
                    FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
                    int allocationTypeSize = allocationTypeSizes.get(atype);
                    nextPaddedOffset = MathUtil.align(nextPaddedOffset, allocationTypeSize);
                    int realOffset = nextPaddedOffset;
                    nextPaddedOffset += allocationTypeSize;
                    if (atype == OOP && firstOopOffset == 0) {
                        firstOopOffset = realOffset;
                    }
                    if (f.contendedGroup().equals("")) {
                        nextPaddedOffset += CONTENDED_PADDING_WIDTH;
                    }
                    result.add(new FieldLayout(f, realOffset, model.sizeOf(f.typeClass())));
                }
                if (!currentGroup.equals("")) {
                    nextPaddedOffset += CONTENDED_PADDING_WIDTH;
                }
            }
        }
        if (isContendedClass) {
            nextPaddedOffset += CONTENDED_PADDING_WIDTH;
        }
        superClassFieldsSize = MathUtil.align(nextPaddedOffset, model.sizeOf("oop"));
        if ((takeHierarchyGaps || takeSuperGaps) && superClassFieldsSize != nextPaddedOffset) {
            superGapsOffsets.add(nextPaddedOffset);
            superGapsSizes.add(superClassFieldsSize - nextPaddedOffset);
        }
    }
    int minAlignment = autoAlign ? 4 : model.objectAlignment();
    for (String k : cd.classHierarchy()) {
        Collection<FieldData> fields = cd.fieldsFor(k);
        for (FieldData f : fields) {
            minAlignment = Math.max(minAlignment, model.sizeOf(f.typeClass()));
        }
    }
    int instanceSize = MathUtil.align(nextPaddedOffset, minAlignment);
    return new ClassLayout(cd, result, model.headerSize(), instanceSize, true);
}

@Override
public ClassLayout layout(ClassData cd) {
    SortedSet<FieldLayout> result = new TreeSet<>();
    if (cd.isArray()) {
        int base = model.arrayHeaderSize();
        int scale = model.sizeOf(cd.arrayComponentType());
        long instanceSize = base + cd.arrayLength() * scale;
        instanceSize = MathUtil.align(instanceSize, model.objectAlignment());
        base = MathUtil.align(base, Math.max(4, scale));
        result.add(new FieldLayout(FieldData.create(cd.arrayClass(), "<elements>", cd.arrayComponentType()), base, scale * cd.arrayLength()));
        return new ClassLayout(cd, result, model.arrayHeaderSize(), instanceSize, false);
    }
    List<ClassData> classDataClassHierarchy = new ArrayList<>();
    ClassData cld = cd;
    classDataClassHierarchy.add(cld);
    while ((cld = cld.superClass()) != null) {
        classDataClassHierarchy.add(0, cld);
    }
    int superClassFieldsSize = 0;
    int nextPaddedOffset = 0;
    for (ClassData clsData : classDataClassHierarchy) {
        EnumMap<FieldAllocationType, Integer> fieldsAllocationCount = new EnumMap<>(FieldAllocationType.class);
        EnumMap<FieldAllocationType, Integer> nextOffset = new EnumMap<>(FieldAllocationType.class);
        EnumMap<FieldAllocationType, ArrayDeque<Integer>> spaceOffset = new EnumMap<>(FieldAllocationType.class);
        EnumMap<FieldAllocationType, Integer> allocationTypeSizes = new EnumMap<>(FieldAllocationType.class);
        for (FieldAllocationType atype : FieldAllocationType.values()) {
            fieldsAllocationCount.put(atype, 0);
            nextOffset.put(atype, 0);
            spaceOffset.put(atype, new ArrayDeque<Integer>());
        }
        allocationTypeSizes.put(OOP, model.sizeOf("oop"));
        allocationTypeSizes.put(BYTE, model.sizeOf("byte"));
        allocationTypeSizes.put(SHORT, model.sizeOf("short"));
        allocationTypeSizes.put(WORD, model.sizeOf("int"));
        allocationTypeSizes.put(DOUBLE, model.sizeOf("long"));
        for (FieldData f : clsData.ownFields()) {
            FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
            Integer count = fieldsAllocationCount.get(atype);
            fieldsAllocationCount.put(atype, ++count);
        }
        int contendedCount = 0;
        EnumMap<FieldAllocationType, Integer> facContended = new EnumMap<>(FieldAllocationType.class);
        for (FieldData f : clsData.ownFields()) {
            FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
            if (f.isContended()) {
                Integer count = facContended.get(atype);
                facContended.put(atype, count == null ? 1 : ++count);
                contendedCount++;
            }
        }
        int nextFieldOffset = (clsData.superClass() == null ? model.headerSize() : 0) + superClassFieldsSize;
        boolean isContendedClass = clsData.isContended();
        if (isContendedClass) {
            nextFieldOffset += CONTENDED_PADDING_WIDTH;
        }
        int doubleCount = fieldsAllocationCount.get(DOUBLE) - (facContended.containsKey(DOUBLE) ? facContended.get(DOUBLE) : 0);
        int wordCount = fieldsAllocationCount.get(WORD) - (facContended.containsKey(WORD) ? facContended.get(WORD) : 0);
        int shortCount = fieldsAllocationCount.get(SHORT) - (facContended.containsKey(SHORT) ? facContended.get(SHORT) : 0);
        int byteCount = fieldsAllocationCount.get(BYTE) - (facContended.containsKey(BYTE) ? facContended.get(BYTE) : 0);
        int oopCount = fieldsAllocationCount.get(OOP) - (facContended.containsKey(OOP) ? facContended.get(OOP) : 0);
        int firstOopOffset = 0;
        boolean compactFields = true;
        int allocationStyle = 1;
        if (PREDEF_OFFSETS.contains(clsData.name())) {
            allocationStyle = 0;
            compactFields = false;
        }
        if (allocationStyle == 0) {
            nextOffset.put(OOP, nextFieldOffset);
            nextOffset.put(DOUBLE, nextOffset.get(OOP) + (oopCount * model.sizeOf("oop")));
        } else {
            nextOffset.put(DOUBLE, nextFieldOffset);
        }
        if (doubleCount > 0) {
            int offset = nextOffset.get(DOUBLE);
            nextOffset.put(DOUBLE, MathUtil.align(offset, allocationTypeSizes.get(DOUBLE)));
            if (offset != nextOffset.get(DOUBLE)) {
                int length = nextOffset.get(DOUBLE) - offset;
                if (compactFields) {
                    if (wordCount > 0) {
                        wordCount -= 1;
                        spaceOffset.get(WORD).push(offset);
                        length -= allocationTypeSizes.get(WORD);
                        offset += allocationTypeSizes.get(WORD);
                    }
                    while (length >= allocationTypeSizes.get(SHORT) && shortCount > 0) {
                        shortCount -= 1;
                        spaceOffset.get(SHORT).push(offset);
                        length -= allocationTypeSizes.get(SHORT);
                        offset += allocationTypeSizes.get(SHORT);
                    }
                    while (length > 0 && byteCount > 0) {
                        byteCount -= 1;
                        spaceOffset.get(BYTE).push(offset);
                        length -= allocationTypeSizes.get(BYTE);
                        offset += allocationTypeSizes.get(BYTE);
                    }
                    if (length >= allocationTypeSizes.get(OOP) && oopCount > 0) {
                        oopCount -= 1;
                        spaceOffset.get(OOP).push(offset);
                    }
                }
            }
        }
        nextOffset.put(WORD, nextOffset.get(DOUBLE) + (doubleCount * allocationTypeSizes.get(DOUBLE)));
        nextOffset.put(SHORT, nextOffset.get(WORD) + (wordCount * allocationTypeSizes.get(WORD)));
        nextOffset.put(BYTE, nextOffset.get(SHORT) + (shortCount * allocationTypeSizes.get(SHORT)));
        nextPaddedOffset = nextOffset.get(BYTE) + byteCount;
        if (allocationStyle == 1) {
            nextOffset.put(OOP, nextPaddedOffset);
            if (oopCount > 0) {
                nextOffset.put(OOP, MathUtil.align(nextOffset.get(OOP), allocationTypeSizes.get(OOP)));
            }
            nextPaddedOffset = nextOffset.get(OOP) + (oopCount * allocationTypeSizes.get(OOP));
        }
        Set<FieldData> layoutedFields = new HashSet<>();
        for (FieldData f : clsData.ownFields()) {
            if (layoutedFields.contains(f))
                continue;
            if (f.isContended())
                continue;
            FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
            int allocationTypeSize = allocationTypeSizes.get(atype);
            Integer allocationTypeSpaceOffset = spaceOffset.get(atype).poll();
            int realOffset;
            if (atype == DOUBLE) {
                int nextDoubleOffset = nextOffset.get(DOUBLE);
                realOffset = nextOffset.get(DOUBLE);
                nextOffset.put(atype, nextDoubleOffset + allocationTypeSize);
            } else {
                if (allocationTypeSpaceOffset != null) {
                    realOffset = allocationTypeSpaceOffset;
                } else {
                    int allocationTypeNextOffset = nextOffset.get(atype);
                    realOffset = allocationTypeNextOffset;
                    nextOffset.put(atype, allocationTypeNextOffset + allocationTypeSize);
                }
            }
            layoutedFields.add(f);
            result.add(new FieldLayout(f, realOffset, model.sizeOf(f.typeClass())));
        }
        if (contendedCount > 0) {
            nextPaddedOffset += CONTENDED_PADDING_WIDTH;
            HashSet<String> contendedGroups = new HashSet<>();
            for (FieldData f : clsData.ownFields()) {
                if (f.isContended()) {
                    contendedGroups.add(f.contendedGroup());
                }
            }
            for (String currentGroup : contendedGroups) {
                for (FieldData f : clsData.ownFields()) {
                    if (layoutedFields.contains(f))
                        continue;
                    if (!f.isContended() || !f.contendedGroup().equals(currentGroup))
                        continue;
                    FieldAllocationType atype = FieldAllocationType.allocationTypeFor(f);
                    int allocationTypeSize = allocationTypeSizes.get(atype);
                    nextPaddedOffset = MathUtil.align(nextPaddedOffset, allocationTypeSize);
                    int realOffset = nextPaddedOffset;
                    nextPaddedOffset += allocationTypeSize;
                    if (atype == OOP && firstOopOffset == 0) {
                        firstOopOffset = realOffset;
                    }
                    if (f.contendedGroup().equals("")) {
                        nextPaddedOffset += CONTENDED_PADDING_WIDTH;
                    }
                    result.add(new FieldLayout(f, realOffset, model.sizeOf(f.typeClass())));
                }
                if (!currentGroup.equals("")) {
                    nextPaddedOffset += CONTENDED_PADDING_WIDTH;
                }
            }
        }
        if (isContendedClass) {
            nextPaddedOffset += CONTENDED_PADDING_WIDTH;
        }
        superClassFieldsSize = MathUtil.align(nextPaddedOffset, model.sizeOf("oop"));
    }
    int minAlignment = model.objectAlignment();
    for (String k : cd.classHierarchy()) {
        Collection<FieldData> fields = cd.fieldsFor(k);
        for (FieldData f : fields) {
            minAlignment = Math.max(minAlignment, model.sizeOf(f.typeClass()));
        }
    }
    int instanceSize = MathUtil.align(nextPaddedOffset, minAlignment);
    return new ClassLayout(cd, result, model.headerSize(), instanceSize, true);
}


@Test
public void testPrint() {
    ClassLayout.parseInstance(new A()).toPrintable();
    ClassLayout.parseInstance(new B()).toPrintable();
    ClassLayout.parseInstance(A.class).toPrintable();
    ClassLayout.parseInstance(B.class).toPrintable();
    ClassLayout.parseInstance(A.class).toPrintable(new A());
    ClassLayout.parseInstance(B.class).toPrintable(new B());
}

@Test
public void testPrint() {
    ClassLayout.parseInstance(new A()).toPrintable();
    ClassLayout.parseInstance(new B()).toPrintable();
    ClassLayout.parseClass(A.class).toPrintable();
    ClassLayout.parseClass(B.class).toPrintable();
    ClassLayout.parseClass(A.class).toPrintable(new A());
    ClassLayout.parseClass(B.class).toPrintable(new B());
}

public String toPrintable(Object instance) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    int maxTypeLen = 5;
    for (FieldLayout f : fields()) {
        maxTypeLen = Math.max(f.typeClass().length(), maxTypeLen);
    }
    int maxDescrLen = 30;
    for (FieldLayout f : fields()) {
        maxDescrLen = Math.max((f.hostClass() + "" + f.name()).length(), maxDescrLen);
    }
    pw.println(classData.name() + " object internals:");
    pw.printf(" %6s %5s %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", "OFFSET", "SIZE", "TYPE", "DESCRIPTION", "VALUE");
    if (instance != null) {
        VirtualMachine vm = VM.current();
        for (long off = 0; off < headerSize(); off += 4) {
            int word = vm.getInt(instance, off);
            pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", off, 4, "", "(object header)", toHex((word >> 0) & 0xFF) + " " + toHex((word >> 8) & 0xFF) + " " + toHex((word >> 16) & 0xFF) + " " + toHex((word >> 24) & 0xFF) + " " + "(" + toBinary((word >> 0) & 0xFF) + " " + toBinary((word >> 8) & 0xFF) + " " + toBinary((word >> 16) & 0xFF) + " " + toBinary((word >> 24) & 0xFF) + ") " + "(" + word + ")");
        }
    } else {
        pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", 0, headerSize(), "", "(object header)", "N/A");
    }
    long nextFree = headerSize();
    long interLoss = 0;
    long exterLoss = 0;
    for (FieldLayout f : fields()) {
        if (f.offset() > nextFree) {
            pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", nextFree, (f.offset() - nextFree), "", "(alignment/padding gap)", "N/A");
            interLoss += (f.offset() - nextFree);
        }
        pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", f.offset(), f.size(), f.typeClass(), f.hostClass() + "." + f.name(), (instance != null) ? f.safeValue(instance) : "N/A");
        nextFree = f.offset() + f.size();
    }
    long sizeOf = VM.current().sizeOf(instance);
    if (sizeOf != nextFree) {
        exterLoss = sizeOf - nextFree;
        pw.printf(" %6d %5s %" + maxTypeLen + "s %s%n", nextFree, exterLoss, "", "(loss due to the next object alignment)");
    }
    pw.printf("Instance size: %d bytes%n", sizeOf);
    pw.printf("Space losses: %d bytes internal + %d bytes external = %d bytes total%n", interLoss, exterLoss, interLoss + exterLoss);
    pw.close();
    return sw.toString();
}

public String toPrintable(Object instance) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    int maxTypeLen = 5;
    for (FieldLayout f : fields()) {
        maxTypeLen = Math.max(f.typeClass().length(), maxTypeLen);
    }
    int maxDescrLen = 30;
    for (FieldLayout f : fields()) {
        maxDescrLen = Math.max((f.hostClass() + "" + f.name()).length(), maxDescrLen);
    }
    if (instance != null) {
        try {
            Class<?> klass = Class.forName(classData.name());
            if (!klass.isAssignableFrom(instance.getClass())) {
                throw new IllegalArgumentException("Passed instance type " + instance.getClass() + " is not assignable from " + klass + ".");
            }
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Class is not found: " + classData.name() + ".");
        }
    }
    pw.println(classData.name() + " object internals:");
    pw.printf(" %6s %5s %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", "OFFSET", "SIZE", "TYPE", "DESCRIPTION", "VALUE");
    if (instance != null) {
        VirtualMachine vm = VM.current();
        for (long off = 0; off < headerSize(); off += 4) {
            int word = vm.getInt(instance, off);
            pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", off, 4, "", "(object header)", toHex((word >> 0) & 0xFF) + " " + toHex((word >> 8) & 0xFF) + " " + toHex((word >> 16) & 0xFF) + " " + toHex((word >> 24) & 0xFF) + " " + "(" + toBinary((word >> 0) & 0xFF) + " " + toBinary((word >> 8) & 0xFF) + " " + toBinary((word >> 16) & 0xFF) + " " + toBinary((word >> 24) & 0xFF) + ") " + "(" + word + ")");
        }
    } else {
        pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", 0, headerSize(), "", "(object header)", "N/A");
    }
    long nextFree = headerSize();
    long interLoss = 0;
    long exterLoss = 0;
    for (FieldLayout f : fields()) {
        if (f.offset() > nextFree) {
            pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", nextFree, (f.offset() - nextFree), "", "(alignment/padding gap)", "N/A");
            interLoss += (f.offset() - nextFree);
        }
        pw.printf(" %6d %5d %" + maxTypeLen + "s %-" + maxDescrLen + "s %s%n", f.offset(), f.size(), f.typeClass(), f.hostClass() + "." + f.name(), (instance != null) ? f.safeValue(instance) : "N/A");
        nextFree = f.offset() + f.size();
    }
    long sizeOf = (instance != null) ? VM.current().sizeOf(instance) : instanceSize();
    ;
    if (sizeOf != nextFree) {
        exterLoss = sizeOf - nextFree;
        pw.printf(" %6d %5s %" + maxTypeLen + "s %s%n", nextFree, exterLoss, "", "(loss due to the next object alignment)");
    }
    pw.printf("Instance size: %d bytes%n", sizeOf);
    pw.printf("Space losses: %d bytes internal + %d bytes external = %d bytes total%n", interLoss, exterLoss, interLoss + exterLoss);
    pw.close();
    return sw.toString();
}


@Test
public void testWithStartPage() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    try {
        PageHelper.startPage(1, 0);
        List<User> list = userMapper.selectAll();
        PageInfo<User> page = new PageInfo<User>(list);
        assertEquals(183, list.size());
        assertEquals(183, page.getTotal());
        PageHelper.startPage(1, -100);
        list = userMapper.selectAll();
        page = new PageInfo<User>(list);
        assertEquals(0, list.size());
        assertEquals(183, page.getTotal());
    } finally {
        sqlSession.close();
    }
}

@Test
public void testWithStartPage() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    try {
        PageHelper.startPage(1, 0);
        List<User> list = userMapper.selectAll();
        PageInfo<User> page = new PageInfo<User>(list);
        assertEquals(0, list.size());
        assertEquals(183, page.getTotal());
        PageHelper.startPage(1, -100);
        list = userMapper.selectAll();
        page = new PageInfo<User>(list);
        assertEquals(0, list.size());
        assertEquals(183, page.getTotal());
    } finally {
        sqlSession.close();
    }
}

@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        if (args.length == 4) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();
        List resultList;
        if (!dialect.skip(ms, parameter, rowBounds)) {
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
            resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if (dialect != null) {
            dialect.afterAll();
        }
    }
}

@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        if (args.length == 4) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();
        List resultList;
        if (!dialect.skip(ms, parameter, rowBounds)) {
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
            resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if (dialect != null) {
            dialect.afterAll();
        }
    }
}


@Test
public void testPageSize10() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
    try {
        PageHelper.startPage(1, 10);
        List<Country> list = countryMapper.selectAll();
        System.out.println(list);
        PageInfo<Country> page = new PageInfo<Country>(list);
        assertEquals(1, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(10, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(8, page.getLastPage());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageSerializable<Country> serializable = PageSerializable.of(list);
        assertEquals(183, serializable.getTotal());
        PageHelper.startPage(2, 10);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(2, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(11, page.getStartRow());
        assertEquals(20, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(8, page.getLastPage());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(19, 10);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(19, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(181, page.getStartRow());
        assertEquals(183, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(12, page.getFirstPage());
        assertEquals(19, page.getLastPage());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(true, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(false, page.isHasNextPage());
    } finally {
        sqlSession.close();
    }
}

@Test
public void testPageSize10() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
    try {
        PageHelper.startPage(1, 10);
        List<Country> list = countryMapper.selectAll();
        System.out.println(list);
        PageInfo<Country> page = new PageInfo<Country>(list);
        assertEquals(1, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(10, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageSerializable<Country> serializable = PageSerializable.of(list);
        assertEquals(183, serializable.getTotal());
        PageHelper.startPage(2, 10);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(2, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(11, page.getStartRow());
        assertEquals(20, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(19, 10);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(19, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(181, page.getStartRow());
        assertEquals(183, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(true, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(false, page.isHasNextPage());
    } finally {
        sqlSession.close();
    }
}

@Deprecated
public int getLastPage() {
    return navigateLastPage;
}

@Test
public void testNavigatePages() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
    try {
        PageHelper.startPage(1, 10);
        List<Country> list = countryMapper.selectAll();
        PageInfo<Country> page = new PageInfo<Country>(list, 20);
        assertEquals(1, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(10, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(19, page.getLastPage());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(2, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list, 2);
        assertEquals(2, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(51, page.getStartRow());
        assertEquals(100, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(2, page.getLastPage());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
    } finally {
        sqlSession.close();
    }
}

@Test
public void testNavigatePages() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
    try {
        PageHelper.startPage(1, 10);
        List<Country> list = countryMapper.selectAll();
        PageInfo<Country> page = new PageInfo<Country>(list, 20);
        assertEquals(1, page.getPageNum());
        assertEquals(10, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(10, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(19, page.getPages());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(2, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list, 2);
        assertEquals(2, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(51, page.getStartRow());
        assertEquals(100, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
    } finally {
        sqlSession.close();
    }
}

@Test
public void testPageSize50() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
    try {
        PageHelper.startPage(1, 50);
        List<Country> list = countryMapper.selectAll();
        PageInfo<Country> page = new PageInfo<Country>(list);
        assertEquals(1, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(50, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(4, page.getLastPage());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(2, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(2, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(51, page.getStartRow());
        assertEquals(100, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(4, page.getLastPage());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(3, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(3, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(101, page.getStartRow());
        assertEquals(150, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(4, page.getLastPage());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(4, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(4, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(151, page.getStartRow());
        assertEquals(183, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(4, page.getLastPage());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(true, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(false, page.isHasNextPage());
    } finally {
        sqlSession.close();
    }
}

@Test
public void testPageSize50() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    CountryMapper countryMapper = sqlSession.getMapper(CountryMapper.class);
    try {
        PageHelper.startPage(1, 50);
        List<Country> list = countryMapper.selectAll();
        PageInfo<Country> page = new PageInfo<Country>(list);
        assertEquals(1, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(50, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(2, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(2, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(51, page.getStartRow());
        assertEquals(100, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(3, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(3, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(101, page.getStartRow());
        assertEquals(150, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());
        PageHelper.startPage(4, 50);
        list = countryMapper.selectAll();
        page = new PageInfo<Country>(list);
        assertEquals(4, page.getPageNum());
        assertEquals(50, page.getPageSize());
        assertEquals(151, page.getStartRow());
        assertEquals(183, page.getEndRow());
        assertEquals(183, page.getTotal());
        assertEquals(4, page.getPages());
        assertEquals(false, page.isIsFirstPage());
        assertEquals(true, page.isIsLastPage());
        assertEquals(true, page.isHasPreviousPage());
        assertEquals(false, page.isHasNextPage());
    } finally {
        sqlSession.close();
    }
}


@Test
public void leftFocusesOnLeftValues() {
    Lens.Simple<Either<String, Integer>, Maybe<String>> left = EitherLens.left();
    assertEquals(just("fail"), view(left, left("fail")));
    assertEquals(nothing(), view(left, right(1)));
    assertEquals(left("foo"), set(left, just("foo"), left("fail")));
    assertEquals(left("fail"), set(left, nothing(), left("fail")));
    assertEquals(left("foo"), set(left, just("foo"), right(1)));
    assertEquals(right(1), set(left, nothing(), right(1)));
}

@Test
public void leftFocusesOnLeftValues() {
    Lens.Simple<Either<String, Integer>, Maybe<String>> left = EitherLens._left();
    assertEquals(just("fail"), view(left, left("fail")));
    assertEquals(nothing(), view(left, right(1)));
    assertEquals(left("foo"), set(left, just("foo"), left("fail")));
    assertEquals(left("fail"), set(left, nothing(), left("fail")));
    assertEquals(left("foo"), set(left, just("foo"), right(1)));
    assertEquals(right(1), set(left, nothing(), right(1)));
}

public static <L, R> Lens.Simple<Either<L, R>, Maybe<L>> left() {
    return simpleLens(CoProduct2::projectA, (lOrR, maybeL) -> maybeL.<Either<L, R>>fmap(Either::left).orElse(lOrR));
}

@Test
public void rightFocusesOnRightValues() {
    Lens.Simple<Either<String, Integer>, Maybe<Integer>> right = EitherLens.right();
    assertEquals(just(1), view(right, right(1)));
    assertEquals(nothing(), view(right, left("fail")));
    assertEquals(right(2), set(right, just(2), right(1)));
    assertEquals(right(1), set(right, nothing(), right(1)));
    assertEquals(right(2), set(right, just(2), left("fail")));
    assertEquals(left("fail"), set(right, nothing(), left("fail")));
}

@Test
public void rightFocusesOnRightValues() {
    Lens.Simple<Either<String, Integer>, Maybe<Integer>> right = EitherLens._right();
    assertEquals(just(1), view(right, right(1)));
    assertEquals(nothing(), view(right, left("fail")));
    assertEquals(right(2), set(right, just(2), right(1)));
    assertEquals(right(1), set(right, nothing(), right(1)));
    assertEquals(right(2), set(right, just(2), left("fail")));
    assertEquals(left("fail"), set(right, nothing(), left("fail")));
}

public static <L, R> Lens.Simple<Either<L, R>, Maybe<R>> right() {
    return simpleLens(CoProduct2::projectB, (lOrR, maybeR) -> maybeR.<Either<L, R>>fmap(Either::right).orElse(lOrR));
}